diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..96bff3b
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,63 @@
+name: Build Release
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ target:
+ - "ttgo-lora32-v21"
+ - "heltec-lora32-v2"
+ - "heltec_wifi_lora_32_V3"
+ - "ESP32_DIY_LoRa"
+ - "ESP32_DIY_1W_LoRa"
+ - "ttgo-t-beam-v1_2"
+ - "ttgo-t-beam-v1"
+ - "ttgo-t-beam-v1_SX1268"
+ - "ttgo-t-beam-v1_2_SX1262"
+ # - "heltec_wireless_stick_lite" # NOT FULLY TESTED
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.9"
+
+ - name: Install PlatformIO Core
+ run: pip install --upgrade platformio
+
+ - name: Build target
+ run: pio run -e ${{ matrix.target }}
+
+ - name: Build FS
+ run: pio run --target buildfs -e ${{ matrix.target }}
+
+ - name: Move Files
+ run: |
+ mkdir -p installer/firmware
+ cp .pio/build/${{ matrix.target }}/firmware.bin installer/firmware/
+ cp .pio/build/${{ matrix.target }}/bootloader.bin installer/firmware/
+ cp .pio/build/${{ matrix.target }}/partitions.bin installer/firmware/
+ cp .pio/build/${{ matrix.target }}/spiffs.bin installer/firmware/
+ cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin installer/firmware/
+
+ - name: Install Zip
+ run: sudo apt-get install zip
+
+ - name: Archive Files
+ run: zip -r installer.zip installer/
+
+ - name: Upload Release Asset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ github.event.release.upload_url }}
+ asset_path: ./installer.zip
+ asset_name: ${{ matrix.target }}.zip
+ asset_content_type: application/zip
diff --git a/.gitignore b/.gitignore
index 4de822f..3d683e2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,7 @@
.vscode/launch.json
.vscode/ipch
.DS_Store
+/data_embed/*.gz
+installer/firmware
+installer/*.bin
+**/__pycache__/
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..f179091
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "editor.tabSize": 4,
+ "editor.formatOnSave": true
+}
diff --git a/data/igate_conf.json b/data/igate_conf.json
index 15ad1fc..9c6974f 100644
--- a/data/igate_conf.json
+++ b/data/igate_conf.json
@@ -3,23 +3,29 @@
"stationMode": 2,
"iGateComment": "LoRa_APRS_iGate",
"wifi": {
+ "autoAP": {
+ "password": "1234567890",
+ "powerOff": 10
+ },
"AP": [
- { "ssid": "WIFI_1",
+ {
+ "ssid": "WIFI_1",
"password": "WIFI_1_password",
- "latitude": 0.0000000,
- "longitude": 0.0000000
+ "latitude": 0.0,
+ "longitude": 0.0
},
- { "ssid": "WIFI_2",
+ {
+ "ssid": "WIFI_2",
"password": "WIFI_2_password",
- "latitude": 0.0000000,
- "longitude": 0.0000000
+ "latitude": 0.0,
+ "longitude": 0.0
}
]
},
"digi": {
"comment": "LoRa_APRS_Digirepeater",
- "latitude": 0.0000000,
- "longitude": 0.0000000
+ "latitude": 0.0,
+ "longitude": 0.0
},
"aprs_is": {
"passcode": "XYZVW",
@@ -39,7 +45,7 @@
"display": {
"alwaysOn": true,
"timeout": 4,
- "turn180" : false
+ "turn180": false
},
"syslog": {
"active": false,
@@ -50,7 +56,7 @@
"active": false
},
"ota": {
- "username":"",
+ "username": "",
"password": ""
},
"other": {
@@ -59,7 +65,7 @@
"igateRepeatsLoRaPackets": false,
"rememberStationTime": 30,
"sendBatteryVoltage": false,
- "externalVoltageMeasurement" : false,
+ "externalVoltageMeasurement": false,
"externalVoltagePin": 34
- }
-}
\ No newline at end of file
+ }
+}
diff --git a/data/process_form.php b/data/process_form.php
deleted file mode 100644
index 4351854..0000000
--- a/data/process_form.php
+++ /dev/null
@@ -1,19 +0,0 @@
- $_POST['name'],
- 'email' => $_POST['email']
- ];
-
- // Convert the data array to JSON format
- $json_data = json_encode($data);
-
- // Specify the path where you want to save the JSON file
- $file_path = 'userdata.json';
-
- // Save the JSON data to the file
- file_put_contents($file_path, $json_data);
-
- echo "Data saved as JSON successfully.";
-}
-?>
\ No newline at end of file
diff --git a/data/script.js b/data/script.js
deleted file mode 100644
index f0914d0..0000000
--- a/data/script.js
+++ /dev/null
@@ -1,34 +0,0 @@
-// Retrieve JSON data from the JSON file in SPIFFS
-fetch('igate_conf.json')
- .then(response => response.json())
- .then(data => {
- // Process the JSON data
- //displayJSONData(data);
-
- const jsonDataDiv = document.getElementById('json-container');
- jsonDataDiv.innerHTML = ''; // Clear the previous content
-
- if (Array.isArray(data)) {
- // If data is an array, iterate through it
- data.forEach(item => {
- const itemDiv = document.createElement('div');
- itemDiv.textContent = JSON.stringify(item, null, 2);
- jsonDataDiv.appendChild(itemDiv);
- });
- } else {
- // If data is not an array, display it directly
- const itemDiv = document.createElement('div');
- itemDiv.textContent = JSON.stringify(data, null, 2);
- jsonDataDiv.appendChild(itemDiv);
- }
- })
- .catch(error => {
- console.error('There was a problem fetching the JSON data:', error);
- });
-
-// Display JSON data in the HTML
-function displayJSONData(data) {
- //const jsonContainer = document.getElementById('json-container');
- //jsonContainer.innerHTML = JSON.stringify(data, null, 2);
-
-}
diff --git a/data/test1.html b/data/test1.html
deleted file mode 100644
index 13db39d..0000000
--- a/data/test1.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
- Save Form Data to JSON
-
-
- Save Form Data to JSON
-
-
-
-
\ No newline at end of file
diff --git a/data/test_info_1.html b/data/test_info_1.html
deleted file mode 100644
index 42411f8..0000000
--- a/data/test_info_1.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
- JSON Data Example
-
-
-
-
-
-
\ No newline at end of file
diff --git a/data_embed/bootstrap.css b/data_embed/bootstrap.css
new file mode 100644
index 0000000..f38d232
--- /dev/null
+++ b/data_embed/bootstrap.css
@@ -0,0 +1,12200 @@
+@charset "UTF-8";
+/*!
+* Bootstrap v5.3.2 (https://getbootstrap.com/)
+* Copyright 2011-2023 The Bootstrap Authors
+* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+*/
+:root,
+[data-bs-theme="light"] {
+ --bs-blue: #0d6efd;
+ --bs-indigo: #6610f2;
+ --bs-purple: #6f42c1;
+ --bs-pink: #d63384;
+ --bs-red: #dc3545;
+ --bs-orange: #fd7e14;
+ --bs-yellow: #ffc107;
+ --bs-green: #198754;
+ --bs-teal: #20c997;
+ --bs-cyan: #0dcaf0;
+ --bs-black: #000;
+ --bs-white: #fff;
+ --bs-gray: #6c757d;
+ --bs-gray-dark: #343a40;
+ --bs-gray-100: #f8f9fa;
+ --bs-gray-200: #e9ecef;
+ --bs-gray-300: #dee2e6;
+ --bs-gray-400: #ced4da;
+ --bs-gray-500: #adb5bd;
+ --bs-gray-600: #6c757d;
+ --bs-gray-700: #495057;
+ --bs-gray-800: #343a40;
+ --bs-gray-900: #212529;
+ --bs-primary: #0d6efd;
+ --bs-secondary: #6c757d;
+ --bs-success: #198754;
+ --bs-info: #0dcaf0;
+ --bs-warning: #ffc107;
+ --bs-danger: #dc3545;
+ --bs-light: #f8f9fa;
+ --bs-dark: #212529;
+ --bs-primary-rgb: 13, 110, 253;
+ --bs-secondary-rgb: 108, 117, 125;
+ --bs-success-rgb: 25, 135, 84;
+ --bs-info-rgb: 13, 202, 240;
+ --bs-warning-rgb: 255, 193, 7;
+ --bs-danger-rgb: 220, 53, 69;
+ --bs-light-rgb: 248, 249, 250;
+ --bs-dark-rgb: 33, 37, 41;
+ --bs-primary-text-emphasis: #052c65;
+ --bs-secondary-text-emphasis: #2b2f32;
+ --bs-success-text-emphasis: #0a3622;
+ --bs-info-text-emphasis: #055160;
+ --bs-warning-text-emphasis: #664d03;
+ --bs-danger-text-emphasis: #58151c;
+ --bs-light-text-emphasis: #495057;
+ --bs-dark-text-emphasis: #495057;
+ --bs-primary-bg-subtle: #cfe2ff;
+ --bs-secondary-bg-subtle: #e2e3e5;
+ --bs-success-bg-subtle: #d1e7dd;
+ --bs-info-bg-subtle: #cff4fc;
+ --bs-warning-bg-subtle: #fff3cd;
+ --bs-danger-bg-subtle: #f8d7da;
+ --bs-light-bg-subtle: #fcfcfd;
+ --bs-dark-bg-subtle: #ced4da;
+ --bs-primary-border-subtle: #9ec5fe;
+ --bs-secondary-border-subtle: #c4c8cb;
+ --bs-success-border-subtle: #a3cfbb;
+ --bs-info-border-subtle: #9eeaf9;
+ --bs-warning-border-subtle: #ffe69c;
+ --bs-danger-border-subtle: #f1aeb5;
+ --bs-light-border-subtle: #e9ecef;
+ --bs-dark-border-subtle: #adb5bd;
+ --bs-white-rgb: 255, 255, 255;
+ --bs-black-rgb: 0, 0, 0;
+ --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI",
+ Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans",
+ Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
+ "Segoe UI Symbol", "Noto Color Emoji";
+ --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas,
+ "Liberation Mono", "Courier New", monospace;
+ --bs-gradient: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.15),
+ rgba(255, 255, 255, 0)
+ );
+ --bs-body-font-family: var(--bs-font-sans-serif);
+ --bs-body-font-size: 1rem;
+ --bs-body-font-weight: 400;
+ --bs-body-line-height: 1.5;
+ --bs-body-color: #212529;
+ --bs-body-color-rgb: 33, 37, 41;
+ --bs-body-bg: #fff;
+ --bs-body-bg-rgb: 255, 255, 255;
+ --bs-emphasis-color: #000;
+ --bs-emphasis-color-rgb: 0, 0, 0;
+ --bs-secondary-color: rgba(33, 37, 41, 0.75);
+ --bs-secondary-color-rgb: 33, 37, 41;
+ --bs-secondary-bg: #e9ecef;
+ --bs-secondary-bg-rgb: 233, 236, 239;
+ --bs-tertiary-color: rgba(33, 37, 41, 0.5);
+ --bs-tertiary-color-rgb: 33, 37, 41;
+ --bs-tertiary-bg: #f8f9fa;
+ --bs-tertiary-bg-rgb: 248, 249, 250;
+ --bs-heading-color: inherit;
+ --bs-link-color: #0d6efd;
+ --bs-link-color-rgb: 13, 110, 253;
+ --bs-link-decoration: underline;
+ --bs-link-hover-color: #0a58ca;
+ --bs-link-hover-color-rgb: 10, 88, 202;
+ --bs-code-color: #d63384;
+ --bs-highlight-color: #212529;
+ --bs-highlight-bg: #fff3cd;
+ --bs-border-width: 1px;
+ --bs-border-style: solid;
+ --bs-border-color: #dee2e6;
+ --bs-border-color-translucent: rgba(0, 0, 0, 0.175);
+ --bs-border-radius: 0.375rem;
+ --bs-border-radius-sm: 0.25rem;
+ --bs-border-radius-lg: 0.5rem;
+ --bs-border-radius-xl: 1rem;
+ --bs-border-radius-xxl: 2rem;
+ --bs-border-radius-2xl: var(--bs-border-radius-xxl);
+ --bs-border-radius-pill: 50rem;
+ --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
+ --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
+ --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
+ --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
+ --bs-focus-ring-width: 0.25rem;
+ --bs-focus-ring-opacity: 0.25;
+ --bs-focus-ring-color: rgba(13, 110, 253, 0.25);
+ --bs-form-valid-color: #198754;
+ --bs-form-valid-border-color: #198754;
+ --bs-form-invalid-color: #dc3545;
+ --bs-form-invalid-border-color: #dc3545;
+}
+[data-bs-theme="dark"] {
+ color-scheme: dark;
+ --bs-body-color: #dee2e6;
+ --bs-body-color-rgb: 222, 226, 230;
+ --bs-body-bg: #212529;
+ --bs-body-bg-rgb: 33, 37, 41;
+ --bs-emphasis-color: #fff;
+ --bs-emphasis-color-rgb: 255, 255, 255;
+ --bs-secondary-color: rgba(222, 226, 230, 0.75);
+ --bs-secondary-color-rgb: 222, 226, 230;
+ --bs-secondary-bg: #343a40;
+ --bs-secondary-bg-rgb: 52, 58, 64;
+ --bs-tertiary-color: rgba(222, 226, 230, 0.5);
+ --bs-tertiary-color-rgb: 222, 226, 230;
+ --bs-tertiary-bg: #2b3035;
+ --bs-tertiary-bg-rgb: 43, 48, 53;
+ --bs-primary-text-emphasis: #6ea8fe;
+ --bs-secondary-text-emphasis: #a7acb1;
+ --bs-success-text-emphasis: #75b798;
+ --bs-info-text-emphasis: #6edff6;
+ --bs-warning-text-emphasis: #ffda6a;
+ --bs-danger-text-emphasis: #ea868f;
+ --bs-light-text-emphasis: #f8f9fa;
+ --bs-dark-text-emphasis: #dee2e6;
+ --bs-primary-bg-subtle: #031633;
+ --bs-secondary-bg-subtle: #161719;
+ --bs-success-bg-subtle: #051b11;
+ --bs-info-bg-subtle: #032830;
+ --bs-warning-bg-subtle: #332701;
+ --bs-danger-bg-subtle: #2c0b0e;
+ --bs-light-bg-subtle: #343a40;
+ --bs-dark-bg-subtle: #1a1d20;
+ --bs-primary-border-subtle: #084298;
+ --bs-secondary-border-subtle: #41464b;
+ --bs-success-border-subtle: #0f5132;
+ --bs-info-border-subtle: #087990;
+ --bs-warning-border-subtle: #997404;
+ --bs-danger-border-subtle: #842029;
+ --bs-light-border-subtle: #495057;
+ --bs-dark-border-subtle: #343a40;
+ --bs-heading-color: inherit;
+ --bs-link-color: #6ea8fe;
+ --bs-link-hover-color: #8bb9fe;
+ --bs-link-color-rgb: 110, 168, 254;
+ --bs-link-hover-color-rgb: 139, 185, 254;
+ --bs-code-color: #e685b5;
+ --bs-highlight-color: #dee2e6;
+ --bs-highlight-bg: #664d03;
+ --bs-border-color: #495057;
+ --bs-border-color-translucent: rgba(255, 255, 255, 0.15);
+ --bs-form-valid-color: #75b798;
+ --bs-form-valid-border-color: #75b798;
+ --bs-form-invalid-color: #ea868f;
+ --bs-form-invalid-border-color: #ea868f;
+}
+*,
+::after,
+::before {
+ box-sizing: border-box;
+}
+@media (prefers-reduced-motion: no-preference) {
+ :root {
+ scroll-behavior: smooth;
+ }
+}
+body {
+ margin: 0;
+ font-family: var(--bs-body-font-family);
+ font-size: var(--bs-body-font-size);
+ font-weight: var(--bs-body-font-weight);
+ line-height: var(--bs-body-line-height);
+ color: var(--bs-body-color);
+ text-align: var(--bs-body-text-align);
+ background-color: var(--bs-body-bg);
+ -webkit-text-size-adjust: 100%;
+ -webkit-tap-highlight-color: transparent;
+}
+hr {
+ margin: 1rem 0;
+ color: inherit;
+ border: 0;
+ border-top: var(--bs-border-width) solid;
+ opacity: 0.25;
+}
+.h1,
+.h2,
+.h3,
+.h4,
+.h5,
+.h6,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin-top: 0;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ line-height: 1.2;
+ color: var(--bs-heading-color);
+}
+.h1,
+h1 {
+ font-size: calc(1.375rem + 1.5vw);
+}
+@media (min-width: 1200px) {
+ .h1,
+ h1 {
+ font-size: 2.5rem;
+ }
+}
+.h2,
+h2 {
+ font-size: calc(1.325rem + 0.9vw);
+}
+@media (min-width: 1200px) {
+ .h2,
+ h2 {
+ font-size: 2rem;
+ }
+}
+.h3,
+h3 {
+ font-size: calc(1.3rem + 0.6vw);
+}
+@media (min-width: 1200px) {
+ .h3,
+ h3 {
+ font-size: 1.75rem;
+ }
+}
+.h4,
+h4 {
+ font-size: calc(1.275rem + 0.3vw);
+}
+@media (min-width: 1200px) {
+ .h4,
+ h4 {
+ font-size: 1.5rem;
+ }
+}
+.h5,
+h5 {
+ font-size: 1.25rem;
+}
+.h6,
+h6 {
+ font-size: 1rem;
+}
+p {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+abbr[title] {
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+ cursor: help;
+ -webkit-text-decoration-skip-ink: none;
+ text-decoration-skip-ink: none;
+}
+address {
+ margin-bottom: 1rem;
+ font-style: normal;
+ line-height: inherit;
+}
+ol,
+ul {
+ padding-left: 2rem;
+}
+dl,
+ol,
+ul {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+ol ol,
+ol ul,
+ul ol,
+ul ul {
+ margin-bottom: 0;
+}
+dt {
+ font-weight: 700;
+}
+dd {
+ margin-bottom: 0.5rem;
+ margin-left: 0;
+}
+blockquote {
+ margin: 0 0 1rem;
+}
+b,
+strong {
+ font-weight: bolder;
+}
+.small,
+small {
+ font-size: 0.875em;
+}
+.mark,
+mark {
+ padding: 0.1875em;
+ color: var(--bs-highlight-color);
+ background-color: var(--bs-highlight-bg);
+}
+sub,
+sup {
+ position: relative;
+ font-size: 0.75em;
+ line-height: 0;
+ vertical-align: baseline;
+}
+sub {
+ bottom: -0.25em;
+}
+sup {
+ top: -0.5em;
+}
+a {
+ color: rgba(
+ var(--bs-link-color-rgb),
+ var(--bs-link-opacity, 1)
+ );
+ text-decoration: underline;
+}
+a:hover {
+ --bs-link-color-rgb: var(--bs-link-hover-color-rgb);
+}
+a:not([href]):not([class]),
+a:not([href]):not([class]):hover {
+ color: inherit;
+ text-decoration: none;
+}
+code,
+kbd,
+pre,
+samp {
+ font-family: var(--bs-font-monospace);
+ font-size: 1em;
+}
+pre {
+ display: block;
+ margin-top: 0;
+ margin-bottom: 1rem;
+ overflow: auto;
+ font-size: 0.875em;
+}
+pre code {
+ font-size: inherit;
+ color: inherit;
+ word-break: normal;
+}
+code {
+ font-size: 0.875em;
+ color: var(--bs-code-color);
+ word-wrap: break-word;
+}
+a > code {
+ color: inherit;
+}
+kbd {
+ padding: 0.1875rem 0.375rem;
+ font-size: 0.875em;
+ color: var(--bs-body-bg);
+ background-color: var(--bs-body-color);
+ border-radius: 0.25rem;
+}
+kbd kbd {
+ padding: 0;
+ font-size: 1em;
+}
+figure {
+ margin: 0 0 1rem;
+}
+img,
+svg {
+ vertical-align: middle;
+}
+table {
+ caption-side: bottom;
+ border-collapse: collapse;
+}
+caption {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ color: var(--bs-secondary-color);
+ text-align: left;
+}
+th {
+ text-align: inherit;
+ text-align: -webkit-match-parent;
+}
+tbody,
+td,
+tfoot,
+th,
+thead,
+tr {
+ border-color: inherit;
+ border-style: solid;
+ border-width: 0;
+}
+label {
+ display: inline-block;
+}
+button {
+ border-radius: 0;
+}
+button:focus:not(:focus-visible) {
+ outline: 0;
+}
+button,
+input,
+optgroup,
+select,
+textarea {
+ margin: 0;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+button,
+select {
+ text-transform: none;
+}
+[role="button"] {
+ cursor: pointer;
+}
+select {
+ word-wrap: normal;
+}
+select:disabled {
+ opacity: 1;
+}
+[list]:not([type="date"]):not([type="datetime-local"]):not(
+ [type="month"]
+ ):not([type="week"]):not(
+ [type="time"]
+ )::-webkit-calendar-picker-indicator {
+ display: none !important;
+}
+[type="button"],
+[type="reset"],
+[type="submit"],
+button {
+ -webkit-appearance: button;
+}
+[type="button"]:not(:disabled),
+[type="reset"]:not(:disabled),
+[type="submit"]:not(:disabled),
+button:not(:disabled) {
+ cursor: pointer;
+}
+::-moz-focus-inner {
+ padding: 0;
+ border-style: none;
+}
+textarea {
+ resize: vertical;
+}
+fieldset {
+ min-width: 0;
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+legend {
+ float: left;
+ width: 100%;
+ padding: 0;
+ margin-bottom: 0.5rem;
+ font-size: calc(1.275rem + 0.3vw);
+ line-height: inherit;
+}
+@media (min-width: 1200px) {
+ legend {
+ font-size: 1.5rem;
+ }
+}
+legend + * {
+ clear: left;
+}
+::-webkit-datetime-edit-day-field,
+::-webkit-datetime-edit-fields-wrapper,
+::-webkit-datetime-edit-hour-field,
+::-webkit-datetime-edit-minute,
+::-webkit-datetime-edit-month-field,
+::-webkit-datetime-edit-text,
+::-webkit-datetime-edit-year-field {
+ padding: 0;
+}
+::-webkit-inner-spin-button {
+ height: auto;
+}
+[type="search"] {
+ -webkit-appearance: textfield;
+ outline-offset: -2px;
+}
+::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+::-webkit-color-swatch-wrapper {
+ padding: 0;
+}
+::-webkit-file-upload-button {
+ font: inherit;
+ -webkit-appearance: button;
+}
+::file-selector-button {
+ font: inherit;
+ -webkit-appearance: button;
+}
+output {
+ display: inline-block;
+}
+iframe {
+ border: 0;
+}
+summary {
+ display: list-item;
+ cursor: pointer;
+}
+progress {
+ vertical-align: baseline;
+}
+[hidden] {
+ display: none !important;
+}
+.lead {
+ font-size: 1.25rem;
+ font-weight: 300;
+}
+.display-1 {
+ font-size: calc(1.625rem + 4.5vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+@media (min-width: 1200px) {
+ .display-1 {
+ font-size: 5rem;
+ }
+}
+.display-2 {
+ font-size: calc(1.575rem + 3.9vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+@media (min-width: 1200px) {
+ .display-2 {
+ font-size: 4.5rem;
+ }
+}
+.display-3 {
+ font-size: calc(1.525rem + 3.3vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+@media (min-width: 1200px) {
+ .display-3 {
+ font-size: 4rem;
+ }
+}
+.display-4 {
+ font-size: calc(1.475rem + 2.7vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+@media (min-width: 1200px) {
+ .display-4 {
+ font-size: 3.5rem;
+ }
+}
+.display-5 {
+ font-size: calc(1.425rem + 2.1vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+@media (min-width: 1200px) {
+ .display-5 {
+ font-size: 3rem;
+ }
+}
+.display-6 {
+ font-size: calc(1.375rem + 1.5vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+@media (min-width: 1200px) {
+ .display-6 {
+ font-size: 2.5rem;
+ }
+}
+.list-unstyled {
+ padding-left: 0;
+ list-style: none;
+}
+.list-inline {
+ padding-left: 0;
+ list-style: none;
+}
+.list-inline-item {
+ display: inline-block;
+}
+.list-inline-item:not(:last-child) {
+ margin-right: 0.5rem;
+}
+.initialism {
+ font-size: 0.875em;
+ text-transform: uppercase;
+}
+.blockquote {
+ margin-bottom: 1rem;
+ font-size: 1.25rem;
+}
+.blockquote > :last-child {
+ margin-bottom: 0;
+}
+.blockquote-footer {
+ margin-top: -1rem;
+ margin-bottom: 1rem;
+ font-size: 0.875em;
+ color: #6c757d;
+}
+.blockquote-footer::before {
+ content: "— ";
+}
+.img-fluid {
+ max-width: 100%;
+ height: auto;
+}
+.img-thumbnail {
+ padding: 0.25rem;
+ background-color: var(--bs-body-bg);
+ border: var(--bs-border-width) solid var(--bs-border-color);
+ border-radius: var(--bs-border-radius);
+ max-width: 100%;
+ height: auto;
+}
+.figure {
+ display: inline-block;
+}
+.figure-img {
+ margin-bottom: 0.5rem;
+ line-height: 1;
+}
+.figure-caption {
+ font-size: 0.875em;
+ color: var(--bs-secondary-color);
+}
+.container,
+.container-fluid,
+.container-lg,
+.container-md,
+.container-sm,
+.container-xl,
+.container-xxl {
+ --bs-gutter-x: 1.5rem;
+ --bs-gutter-y: 0;
+ width: 100%;
+ padding-right: calc(var(--bs-gutter-x) * 0.5);
+ padding-left: calc(var(--bs-gutter-x) * 0.5);
+ margin-right: auto;
+ margin-left: auto;
+}
+@media (min-width: 576px) {
+ .container,
+ .container-sm {
+ max-width: 540px;
+ }
+}
+@media (min-width: 768px) {
+ .container,
+ .container-md,
+ .container-sm {
+ max-width: 720px;
+ }
+}
+@media (min-width: 992px) {
+ .container,
+ .container-lg,
+ .container-md,
+ .container-sm {
+ max-width: 960px;
+ }
+}
+@media (min-width: 1200px) {
+ .container,
+ .container-lg,
+ .container-md,
+ .container-sm,
+ .container-xl {
+ max-width: 1140px;
+ }
+}
+@media (min-width: 1400px) {
+ .container,
+ .container-lg,
+ .container-md,
+ .container-sm,
+ .container-xl,
+ .container-xxl {
+ max-width: 1320px;
+ }
+}
+:root {
+ --bs-breakpoint-xs: 0;
+ --bs-breakpoint-sm: 576px;
+ --bs-breakpoint-md: 768px;
+ --bs-breakpoint-lg: 992px;
+ --bs-breakpoint-xl: 1200px;
+ --bs-breakpoint-xxl: 1400px;
+}
+.row {
+ --bs-gutter-x: 1.5rem;
+ --bs-gutter-y: 0;
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: calc(-1 * var(--bs-gutter-y));
+ margin-right: calc(-0.5 * var(--bs-gutter-x));
+ margin-left: calc(-0.5 * var(--bs-gutter-x));
+}
+.row > * {
+ flex-shrink: 0;
+ width: 100%;
+ max-width: 100%;
+ padding-right: calc(var(--bs-gutter-x) * 0.5);
+ padding-left: calc(var(--bs-gutter-x) * 0.5);
+ margin-top: var(--bs-gutter-y);
+}
+.col {
+ flex: 1 0 0%;
+}
+.row-cols-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+}
+.row-cols-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+}
+.row-cols-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+}
+.row-cols-3 > * {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+}
+.row-cols-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+}
+.row-cols-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+}
+.row-cols-6 > * {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+}
+.col-auto {
+ flex: 0 0 auto;
+ width: auto;
+}
+.col-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+}
+.col-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+}
+.col-3 {
+ flex: 0 0 auto;
+ width: 25%;
+}
+.col-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+}
+.col-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+}
+.col-6 {
+ flex: 0 0 auto;
+ width: 50%;
+}
+.col-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+}
+.col-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+}
+.col-9 {
+ flex: 0 0 auto;
+ width: 75%;
+}
+.col-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+}
+.col-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+}
+.col-12 {
+ flex: 0 0 auto;
+ width: 100%;
+}
+.offset-1 {
+ margin-left: 8.33333333%;
+}
+.offset-2 {
+ margin-left: 16.66666667%;
+}
+.offset-3 {
+ margin-left: 25%;
+}
+.offset-4 {
+ margin-left: 33.33333333%;
+}
+.offset-5 {
+ margin-left: 41.66666667%;
+}
+.offset-6 {
+ margin-left: 50%;
+}
+.offset-7 {
+ margin-left: 58.33333333%;
+}
+.offset-8 {
+ margin-left: 66.66666667%;
+}
+.offset-9 {
+ margin-left: 75%;
+}
+.offset-10 {
+ margin-left: 83.33333333%;
+}
+.offset-11 {
+ margin-left: 91.66666667%;
+}
+.g-0,
+.gx-0 {
+ --bs-gutter-x: 0;
+}
+.g-0,
+.gy-0 {
+ --bs-gutter-y: 0;
+}
+.g-1,
+.gx-1 {
+ --bs-gutter-x: 0.25rem;
+}
+.g-1,
+.gy-1 {
+ --bs-gutter-y: 0.25rem;
+}
+.g-2,
+.gx-2 {
+ --bs-gutter-x: 0.5rem;
+}
+.g-2,
+.gy-2 {
+ --bs-gutter-y: 0.5rem;
+}
+.g-3,
+.gx-3 {
+ --bs-gutter-x: 1rem;
+}
+.g-3,
+.gy-3 {
+ --bs-gutter-y: 1rem;
+}
+.g-4,
+.gx-4 {
+ --bs-gutter-x: 1.5rem;
+}
+.g-4,
+.gy-4 {
+ --bs-gutter-y: 1.5rem;
+}
+.g-5,
+.gx-5 {
+ --bs-gutter-x: 3rem;
+}
+.g-5,
+.gy-5 {
+ --bs-gutter-y: 3rem;
+}
+@media (min-width: 576px) {
+ .col-sm {
+ flex: 1 0 0%;
+ }
+ .row-cols-sm-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+ }
+ .row-cols-sm-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+ .row-cols-sm-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+ .row-cols-sm-3 > * {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+ .row-cols-sm-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+ .row-cols-sm-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+ }
+ .row-cols-sm-6 > * {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+ .col-sm-auto {
+ flex: 0 0 auto;
+ width: auto;
+ }
+ .col-sm-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+ }
+ .col-sm-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+ .col-sm-3 {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+ .col-sm-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+ .col-sm-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+ }
+ .col-sm-6 {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+ .col-sm-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+ }
+ .col-sm-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+ }
+ .col-sm-9 {
+ flex: 0 0 auto;
+ width: 75%;
+ }
+ .col-sm-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+ }
+ .col-sm-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+ }
+ .col-sm-12 {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+ .offset-sm-0 {
+ margin-left: 0;
+ }
+ .offset-sm-1 {
+ margin-left: 8.33333333%;
+ }
+ .offset-sm-2 {
+ margin-left: 16.66666667%;
+ }
+ .offset-sm-3 {
+ margin-left: 25%;
+ }
+ .offset-sm-4 {
+ margin-left: 33.33333333%;
+ }
+ .offset-sm-5 {
+ margin-left: 41.66666667%;
+ }
+ .offset-sm-6 {
+ margin-left: 50%;
+ }
+ .offset-sm-7 {
+ margin-left: 58.33333333%;
+ }
+ .offset-sm-8 {
+ margin-left: 66.66666667%;
+ }
+ .offset-sm-9 {
+ margin-left: 75%;
+ }
+ .offset-sm-10 {
+ margin-left: 83.33333333%;
+ }
+ .offset-sm-11 {
+ margin-left: 91.66666667%;
+ }
+ .g-sm-0,
+ .gx-sm-0 {
+ --bs-gutter-x: 0;
+ }
+ .g-sm-0,
+ .gy-sm-0 {
+ --bs-gutter-y: 0;
+ }
+ .g-sm-1,
+ .gx-sm-1 {
+ --bs-gutter-x: 0.25rem;
+ }
+ .g-sm-1,
+ .gy-sm-1 {
+ --bs-gutter-y: 0.25rem;
+ }
+ .g-sm-2,
+ .gx-sm-2 {
+ --bs-gutter-x: 0.5rem;
+ }
+ .g-sm-2,
+ .gy-sm-2 {
+ --bs-gutter-y: 0.5rem;
+ }
+ .g-sm-3,
+ .gx-sm-3 {
+ --bs-gutter-x: 1rem;
+ }
+ .g-sm-3,
+ .gy-sm-3 {
+ --bs-gutter-y: 1rem;
+ }
+ .g-sm-4,
+ .gx-sm-4 {
+ --bs-gutter-x: 1.5rem;
+ }
+ .g-sm-4,
+ .gy-sm-4 {
+ --bs-gutter-y: 1.5rem;
+ }
+ .g-sm-5,
+ .gx-sm-5 {
+ --bs-gutter-x: 3rem;
+ }
+ .g-sm-5,
+ .gy-sm-5 {
+ --bs-gutter-y: 3rem;
+ }
+}
+@media (min-width: 768px) {
+ .col-md {
+ flex: 1 0 0%;
+ }
+ .row-cols-md-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+ }
+ .row-cols-md-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+ .row-cols-md-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+ .row-cols-md-3 > * {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+ .row-cols-md-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+ .row-cols-md-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+ }
+ .row-cols-md-6 > * {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+ .col-md-auto {
+ flex: 0 0 auto;
+ width: auto;
+ }
+ .col-md-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+ }
+ .col-md-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+ .col-md-3 {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+ .col-md-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+ .col-md-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+ }
+ .col-md-6 {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+ .col-md-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+ }
+ .col-md-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+ }
+ .col-md-9 {
+ flex: 0 0 auto;
+ width: 75%;
+ }
+ .col-md-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+ }
+ .col-md-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+ }
+ .col-md-12 {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+ .offset-md-0 {
+ margin-left: 0;
+ }
+ .offset-md-1 {
+ margin-left: 8.33333333%;
+ }
+ .offset-md-2 {
+ margin-left: 16.66666667%;
+ }
+ .offset-md-3 {
+ margin-left: 25%;
+ }
+ .offset-md-4 {
+ margin-left: 33.33333333%;
+ }
+ .offset-md-5 {
+ margin-left: 41.66666667%;
+ }
+ .offset-md-6 {
+ margin-left: 50%;
+ }
+ .offset-md-7 {
+ margin-left: 58.33333333%;
+ }
+ .offset-md-8 {
+ margin-left: 66.66666667%;
+ }
+ .offset-md-9 {
+ margin-left: 75%;
+ }
+ .offset-md-10 {
+ margin-left: 83.33333333%;
+ }
+ .offset-md-11 {
+ margin-left: 91.66666667%;
+ }
+ .g-md-0,
+ .gx-md-0 {
+ --bs-gutter-x: 0;
+ }
+ .g-md-0,
+ .gy-md-0 {
+ --bs-gutter-y: 0;
+ }
+ .g-md-1,
+ .gx-md-1 {
+ --bs-gutter-x: 0.25rem;
+ }
+ .g-md-1,
+ .gy-md-1 {
+ --bs-gutter-y: 0.25rem;
+ }
+ .g-md-2,
+ .gx-md-2 {
+ --bs-gutter-x: 0.5rem;
+ }
+ .g-md-2,
+ .gy-md-2 {
+ --bs-gutter-y: 0.5rem;
+ }
+ .g-md-3,
+ .gx-md-3 {
+ --bs-gutter-x: 1rem;
+ }
+ .g-md-3,
+ .gy-md-3 {
+ --bs-gutter-y: 1rem;
+ }
+ .g-md-4,
+ .gx-md-4 {
+ --bs-gutter-x: 1.5rem;
+ }
+ .g-md-4,
+ .gy-md-4 {
+ --bs-gutter-y: 1.5rem;
+ }
+ .g-md-5,
+ .gx-md-5 {
+ --bs-gutter-x: 3rem;
+ }
+ .g-md-5,
+ .gy-md-5 {
+ --bs-gutter-y: 3rem;
+ }
+}
+@media (min-width: 992px) {
+ .col-lg {
+ flex: 1 0 0%;
+ }
+ .row-cols-lg-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+ }
+ .row-cols-lg-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+ .row-cols-lg-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+ .row-cols-lg-3 > * {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+ .row-cols-lg-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+ .row-cols-lg-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+ }
+ .row-cols-lg-6 > * {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+ .col-lg-auto {
+ flex: 0 0 auto;
+ width: auto;
+ }
+ .col-lg-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+ }
+ .col-lg-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+ .col-lg-3 {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+ .col-lg-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+ .col-lg-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+ }
+ .col-lg-6 {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+ .col-lg-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+ }
+ .col-lg-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+ }
+ .col-lg-9 {
+ flex: 0 0 auto;
+ width: 75%;
+ }
+ .col-lg-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+ }
+ .col-lg-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+ }
+ .col-lg-12 {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+ .offset-lg-0 {
+ margin-left: 0;
+ }
+ .offset-lg-1 {
+ margin-left: 8.33333333%;
+ }
+ .offset-lg-2 {
+ margin-left: 16.66666667%;
+ }
+ .offset-lg-3 {
+ margin-left: 25%;
+ }
+ .offset-lg-4 {
+ margin-left: 33.33333333%;
+ }
+ .offset-lg-5 {
+ margin-left: 41.66666667%;
+ }
+ .offset-lg-6 {
+ margin-left: 50%;
+ }
+ .offset-lg-7 {
+ margin-left: 58.33333333%;
+ }
+ .offset-lg-8 {
+ margin-left: 66.66666667%;
+ }
+ .offset-lg-9 {
+ margin-left: 75%;
+ }
+ .offset-lg-10 {
+ margin-left: 83.33333333%;
+ }
+ .offset-lg-11 {
+ margin-left: 91.66666667%;
+ }
+ .g-lg-0,
+ .gx-lg-0 {
+ --bs-gutter-x: 0;
+ }
+ .g-lg-0,
+ .gy-lg-0 {
+ --bs-gutter-y: 0;
+ }
+ .g-lg-1,
+ .gx-lg-1 {
+ --bs-gutter-x: 0.25rem;
+ }
+ .g-lg-1,
+ .gy-lg-1 {
+ --bs-gutter-y: 0.25rem;
+ }
+ .g-lg-2,
+ .gx-lg-2 {
+ --bs-gutter-x: 0.5rem;
+ }
+ .g-lg-2,
+ .gy-lg-2 {
+ --bs-gutter-y: 0.5rem;
+ }
+ .g-lg-3,
+ .gx-lg-3 {
+ --bs-gutter-x: 1rem;
+ }
+ .g-lg-3,
+ .gy-lg-3 {
+ --bs-gutter-y: 1rem;
+ }
+ .g-lg-4,
+ .gx-lg-4 {
+ --bs-gutter-x: 1.5rem;
+ }
+ .g-lg-4,
+ .gy-lg-4 {
+ --bs-gutter-y: 1.5rem;
+ }
+ .g-lg-5,
+ .gx-lg-5 {
+ --bs-gutter-x: 3rem;
+ }
+ .g-lg-5,
+ .gy-lg-5 {
+ --bs-gutter-y: 3rem;
+ }
+}
+@media (min-width: 1200px) {
+ .col-xl {
+ flex: 1 0 0%;
+ }
+ .row-cols-xl-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+ }
+ .row-cols-xl-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+ .row-cols-xl-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+ .row-cols-xl-3 > * {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+ .row-cols-xl-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+ .row-cols-xl-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+ }
+ .row-cols-xl-6 > * {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+ .col-xl-auto {
+ flex: 0 0 auto;
+ width: auto;
+ }
+ .col-xl-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+ }
+ .col-xl-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+ .col-xl-3 {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+ .col-xl-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+ .col-xl-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+ }
+ .col-xl-6 {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+ .col-xl-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+ }
+ .col-xl-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+ }
+ .col-xl-9 {
+ flex: 0 0 auto;
+ width: 75%;
+ }
+ .col-xl-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+ }
+ .col-xl-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+ }
+ .col-xl-12 {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+ .offset-xl-0 {
+ margin-left: 0;
+ }
+ .offset-xl-1 {
+ margin-left: 8.33333333%;
+ }
+ .offset-xl-2 {
+ margin-left: 16.66666667%;
+ }
+ .offset-xl-3 {
+ margin-left: 25%;
+ }
+ .offset-xl-4 {
+ margin-left: 33.33333333%;
+ }
+ .offset-xl-5 {
+ margin-left: 41.66666667%;
+ }
+ .offset-xl-6 {
+ margin-left: 50%;
+ }
+ .offset-xl-7 {
+ margin-left: 58.33333333%;
+ }
+ .offset-xl-8 {
+ margin-left: 66.66666667%;
+ }
+ .offset-xl-9 {
+ margin-left: 75%;
+ }
+ .offset-xl-10 {
+ margin-left: 83.33333333%;
+ }
+ .offset-xl-11 {
+ margin-left: 91.66666667%;
+ }
+ .g-xl-0,
+ .gx-xl-0 {
+ --bs-gutter-x: 0;
+ }
+ .g-xl-0,
+ .gy-xl-0 {
+ --bs-gutter-y: 0;
+ }
+ .g-xl-1,
+ .gx-xl-1 {
+ --bs-gutter-x: 0.25rem;
+ }
+ .g-xl-1,
+ .gy-xl-1 {
+ --bs-gutter-y: 0.25rem;
+ }
+ .g-xl-2,
+ .gx-xl-2 {
+ --bs-gutter-x: 0.5rem;
+ }
+ .g-xl-2,
+ .gy-xl-2 {
+ --bs-gutter-y: 0.5rem;
+ }
+ .g-xl-3,
+ .gx-xl-3 {
+ --bs-gutter-x: 1rem;
+ }
+ .g-xl-3,
+ .gy-xl-3 {
+ --bs-gutter-y: 1rem;
+ }
+ .g-xl-4,
+ .gx-xl-4 {
+ --bs-gutter-x: 1.5rem;
+ }
+ .g-xl-4,
+ .gy-xl-4 {
+ --bs-gutter-y: 1.5rem;
+ }
+ .g-xl-5,
+ .gx-xl-5 {
+ --bs-gutter-x: 3rem;
+ }
+ .g-xl-5,
+ .gy-xl-5 {
+ --bs-gutter-y: 3rem;
+ }
+}
+@media (min-width: 1400px) {
+ .col-xxl {
+ flex: 1 0 0%;
+ }
+ .row-cols-xxl-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+ }
+ .row-cols-xxl-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+ .row-cols-xxl-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+ .row-cols-xxl-3 > * {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+ .row-cols-xxl-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+ .row-cols-xxl-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+ }
+ .row-cols-xxl-6 > * {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+ .col-xxl-auto {
+ flex: 0 0 auto;
+ width: auto;
+ }
+ .col-xxl-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+ }
+ .col-xxl-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+ .col-xxl-3 {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+ .col-xxl-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+ .col-xxl-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+ }
+ .col-xxl-6 {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+ .col-xxl-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+ }
+ .col-xxl-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+ }
+ .col-xxl-9 {
+ flex: 0 0 auto;
+ width: 75%;
+ }
+ .col-xxl-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+ }
+ .col-xxl-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+ }
+ .col-xxl-12 {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+ .offset-xxl-0 {
+ margin-left: 0;
+ }
+ .offset-xxl-1 {
+ margin-left: 8.33333333%;
+ }
+ .offset-xxl-2 {
+ margin-left: 16.66666667%;
+ }
+ .offset-xxl-3 {
+ margin-left: 25%;
+ }
+ .offset-xxl-4 {
+ margin-left: 33.33333333%;
+ }
+ .offset-xxl-5 {
+ margin-left: 41.66666667%;
+ }
+ .offset-xxl-6 {
+ margin-left: 50%;
+ }
+ .offset-xxl-7 {
+ margin-left: 58.33333333%;
+ }
+ .offset-xxl-8 {
+ margin-left: 66.66666667%;
+ }
+ .offset-xxl-9 {
+ margin-left: 75%;
+ }
+ .offset-xxl-10 {
+ margin-left: 83.33333333%;
+ }
+ .offset-xxl-11 {
+ margin-left: 91.66666667%;
+ }
+ .g-xxl-0,
+ .gx-xxl-0 {
+ --bs-gutter-x: 0;
+ }
+ .g-xxl-0,
+ .gy-xxl-0 {
+ --bs-gutter-y: 0;
+ }
+ .g-xxl-1,
+ .gx-xxl-1 {
+ --bs-gutter-x: 0.25rem;
+ }
+ .g-xxl-1,
+ .gy-xxl-1 {
+ --bs-gutter-y: 0.25rem;
+ }
+ .g-xxl-2,
+ .gx-xxl-2 {
+ --bs-gutter-x: 0.5rem;
+ }
+ .g-xxl-2,
+ .gy-xxl-2 {
+ --bs-gutter-y: 0.5rem;
+ }
+ .g-xxl-3,
+ .gx-xxl-3 {
+ --bs-gutter-x: 1rem;
+ }
+ .g-xxl-3,
+ .gy-xxl-3 {
+ --bs-gutter-y: 1rem;
+ }
+ .g-xxl-4,
+ .gx-xxl-4 {
+ --bs-gutter-x: 1.5rem;
+ }
+ .g-xxl-4,
+ .gy-xxl-4 {
+ --bs-gutter-y: 1.5rem;
+ }
+ .g-xxl-5,
+ .gx-xxl-5 {
+ --bs-gutter-x: 3rem;
+ }
+ .g-xxl-5,
+ .gy-xxl-5 {
+ --bs-gutter-y: 3rem;
+ }
+}
+.table {
+ --bs-table-color-type: initial;
+ --bs-table-bg-type: initial;
+ --bs-table-color-state: initial;
+ --bs-table-bg-state: initial;
+ --bs-table-color: var(--bs-emphasis-color);
+ --bs-table-bg: var(--bs-body-bg);
+ --bs-table-border-color: var(--bs-border-color);
+ --bs-table-accent-bg: transparent;
+ --bs-table-striped-color: var(--bs-emphasis-color);
+ --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);
+ --bs-table-active-color: var(--bs-emphasis-color);
+ --bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);
+ --bs-table-hover-color: var(--bs-emphasis-color);
+ --bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);
+ width: 100%;
+ margin-bottom: 1rem;
+ vertical-align: top;
+ border-color: var(--bs-table-border-color);
+}
+.table > :not(caption) > * > * {
+ padding: 0.5rem 0.5rem;
+ color: var(
+ --bs-table-color-state,
+ var(--bs-table-color-type, var(--bs-table-color))
+ );
+ background-color: var(--bs-table-bg);
+ border-bottom-width: var(--bs-border-width);
+ box-shadow: inset 0 0 0 9999px
+ var(
+ --bs-table-bg-state,
+ var(--bs-table-bg-type, var(--bs-table-accent-bg))
+ );
+}
+.table > tbody {
+ vertical-align: inherit;
+}
+.table > thead {
+ vertical-align: bottom;
+}
+.table-group-divider {
+ border-top: calc(var(--bs-border-width) * 2) solid currentcolor;
+}
+.caption-top {
+ caption-side: top;
+}
+.table-sm > :not(caption) > * > * {
+ padding: 0.25rem 0.25rem;
+}
+.table-bordered > :not(caption) > * {
+ border-width: var(--bs-border-width) 0;
+}
+.table-bordered > :not(caption) > * > * {
+ border-width: 0 var(--bs-border-width);
+}
+.table-borderless > :not(caption) > * > * {
+ border-bottom-width: 0;
+}
+.table-borderless > :not(:first-child) {
+ border-top-width: 0;
+}
+.table-striped > tbody > tr:nth-of-type(odd) > * {
+ --bs-table-color-type: var(--bs-table-striped-color);
+ --bs-table-bg-type: var(--bs-table-striped-bg);
+}
+.table-striped-columns > :not(caption) > tr > :nth-child(2n) {
+ --bs-table-color-type: var(--bs-table-striped-color);
+ --bs-table-bg-type: var(--bs-table-striped-bg);
+}
+.table-active {
+ --bs-table-color-state: var(--bs-table-active-color);
+ --bs-table-bg-state: var(--bs-table-active-bg);
+}
+.table-hover > tbody > tr:hover > * {
+ --bs-table-color-state: var(--bs-table-hover-color);
+ --bs-table-bg-state: var(--bs-table-hover-bg);
+}
+.table-primary {
+ --bs-table-color: #000;
+ --bs-table-bg: #cfe2ff;
+ --bs-table-border-color: #a6b5cc;
+ --bs-table-striped-bg: #c5d7f2;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #bacbe6;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #bfd1ec;
+ --bs-table-hover-color: #000;
+ color: var(--bs-table-color);
+ border-color: var(--bs-table-border-color);
+}
+.table-secondary {
+ --bs-table-color: #000;
+ --bs-table-bg: #e2e3e5;
+ --bs-table-border-color: #b5b6b7;
+ --bs-table-striped-bg: #d7d8da;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #cbccce;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #d1d2d4;
+ --bs-table-hover-color: #000;
+ color: var(--bs-table-color);
+ border-color: var(--bs-table-border-color);
+}
+.table-success {
+ --bs-table-color: #000;
+ --bs-table-bg: #d1e7dd;
+ --bs-table-border-color: #a7b9b1;
+ --bs-table-striped-bg: #c7dbd2;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #bcd0c7;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #c1d6cc;
+ --bs-table-hover-color: #000;
+ color: var(--bs-table-color);
+ border-color: var(--bs-table-border-color);
+}
+.table-info {
+ --bs-table-color: #000;
+ --bs-table-bg: #cff4fc;
+ --bs-table-border-color: #a6c3ca;
+ --bs-table-striped-bg: #c5e8ef;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #badce3;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #bfe2e9;
+ --bs-table-hover-color: #000;
+ color: var(--bs-table-color);
+ border-color: var(--bs-table-border-color);
+}
+.table-warning {
+ --bs-table-color: #000;
+ --bs-table-bg: #fff3cd;
+ --bs-table-border-color: #ccc2a4;
+ --bs-table-striped-bg: #f2e7c3;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #e6dbb9;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #ece1be;
+ --bs-table-hover-color: #000;
+ color: var(--bs-table-color);
+ border-color: var(--bs-table-border-color);
+}
+.table-danger {
+ --bs-table-color: #000;
+ --bs-table-bg: #f8d7da;
+ --bs-table-border-color: #c6acae;
+ --bs-table-striped-bg: #eccccf;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #dfc2c4;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #e5c7ca;
+ --bs-table-hover-color: #000;
+ color: var(--bs-table-color);
+ border-color: var(--bs-table-border-color);
+}
+.table-light {
+ --bs-table-color: #000;
+ --bs-table-bg: #f8f9fa;
+ --bs-table-border-color: #c6c7c8;
+ --bs-table-striped-bg: #ecedee;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #dfe0e1;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #e5e6e7;
+ --bs-table-hover-color: #000;
+ color: var(--bs-table-color);
+ border-color: var(--bs-table-border-color);
+}
+.table-dark {
+ --bs-table-color: #fff;
+ --bs-table-bg: #212529;
+ --bs-table-border-color: #4d5154;
+ --bs-table-striped-bg: #2c3034;
+ --bs-table-striped-color: #fff;
+ --bs-table-active-bg: #373b3e;
+ --bs-table-active-color: #fff;
+ --bs-table-hover-bg: #323539;
+ --bs-table-hover-color: #fff;
+ color: var(--bs-table-color);
+ border-color: var(--bs-table-border-color);
+}
+.table-responsive {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+@media (max-width: 575.98px) {
+ .table-responsive-sm {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+@media (max-width: 767.98px) {
+ .table-responsive-md {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+@media (max-width: 991.98px) {
+ .table-responsive-lg {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+@media (max-width: 1199.98px) {
+ .table-responsive-xl {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+@media (max-width: 1399.98px) {
+ .table-responsive-xxl {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+.form-label {
+ margin-bottom: 0.5rem;
+}
+.col-form-label {
+ padding-top: calc(0.375rem + var(--bs-border-width));
+ padding-bottom: calc(0.375rem + var(--bs-border-width));
+ margin-bottom: 0;
+ font-size: inherit;
+ line-height: 1.5;
+}
+.col-form-label-lg {
+ padding-top: calc(0.5rem + var(--bs-border-width));
+ padding-bottom: calc(0.5rem + var(--bs-border-width));
+ font-size: 1.25rem;
+}
+.col-form-label-sm {
+ padding-top: calc(0.25rem + var(--bs-border-width));
+ padding-bottom: calc(0.25rem + var(--bs-border-width));
+ font-size: 0.875rem;
+}
+.form-text {
+ margin-top: 0.25rem;
+ font-size: 0.875em;
+ color: var(--bs-secondary-color);
+}
+.form-control {
+ display: block;
+ width: 100%;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: var(--bs-body-color);
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: var(--bs-body-bg);
+ background-clip: padding-box;
+ border: var(--bs-border-width) solid var(--bs-border-color);
+ border-radius: var(--bs-border-radius);
+ transition: border-color 0.15s ease-in-out,
+ box-shadow 0.15s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .form-control {
+ transition: none;
+ }
+}
+.form-control[type="file"] {
+ overflow: hidden;
+}
+.form-control[type="file"]:not(:disabled):not([readonly]) {
+ cursor: pointer;
+}
+.form-control:focus {
+ color: var(--bs-body-color);
+ background-color: var(--bs-body-bg);
+ border-color: #86b7fe;
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+.form-control::-webkit-date-and-time-value {
+ min-width: 85px;
+ height: 1.5em;
+ margin: 0;
+}
+.form-control::-webkit-datetime-edit {
+ display: block;
+ padding: 0;
+}
+.form-control::-moz-placeholder {
+ color: var(--bs-secondary-color);
+ opacity: 1;
+}
+.form-control::placeholder {
+ color: var(--bs-secondary-color);
+ opacity: 1;
+}
+.form-control:disabled {
+ background-color: var(--bs-secondary-bg);
+ opacity: 1;
+}
+.form-control::-webkit-file-upload-button {
+ padding: 0.375rem 0.75rem;
+ margin: -0.375rem -0.75rem;
+ -webkit-margin-end: 0.75rem;
+ margin-inline-end: 0.75rem;
+ color: var(--bs-body-color);
+ background-color: var(--bs-tertiary-bg);
+ pointer-events: none;
+ border-color: inherit;
+ border-style: solid;
+ border-width: 0;
+ border-inline-end-width: var(--bs-border-width);
+ border-radius: 0;
+ -webkit-transition: color 0.15s ease-in-out,
+ background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out,
+ background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+.form-control::file-selector-button {
+ padding: 0.375rem 0.75rem;
+ margin: -0.375rem -0.75rem;
+ -webkit-margin-end: 0.75rem;
+ margin-inline-end: 0.75rem;
+ color: var(--bs-body-color);
+ background-color: var(--bs-tertiary-bg);
+ pointer-events: none;
+ border-color: inherit;
+ border-style: solid;
+ border-width: 0;
+ border-inline-end-width: var(--bs-border-width);
+ border-radius: 0;
+ transition: color 0.15s ease-in-out,
+ background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .form-control::-webkit-file-upload-button {
+ -webkit-transition: none;
+ transition: none;
+ }
+ .form-control::file-selector-button {
+ transition: none;
+ }
+}
+.form-control:hover:not(:disabled):not(
+ [readonly]
+ )::-webkit-file-upload-button {
+ background-color: var(--bs-secondary-bg);
+}
+.form-control:hover:not(:disabled):not(
+ [readonly]
+ )::file-selector-button {
+ background-color: var(--bs-secondary-bg);
+}
+.form-control-plaintext {
+ display: block;
+ width: 100%;
+ padding: 0.375rem 0;
+ margin-bottom: 0;
+ line-height: 1.5;
+ color: var(--bs-body-color);
+ background-color: transparent;
+ border: solid transparent;
+ border-width: var(--bs-border-width) 0;
+}
+.form-control-plaintext:focus {
+ outline: 0;
+}
+.form-control-plaintext.form-control-lg,
+.form-control-plaintext.form-control-sm {
+ padding-right: 0;
+ padding-left: 0;
+}
+.form-control-sm {
+ min-height: calc(
+ 1.5em + 0.5rem + calc(var(--bs-border-width) * 2)
+ );
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ border-radius: var(--bs-border-radius-sm);
+}
+.form-control-sm::-webkit-file-upload-button {
+ padding: 0.25rem 0.5rem;
+ margin: -0.25rem -0.5rem;
+ -webkit-margin-end: 0.5rem;
+ margin-inline-end: 0.5rem;
+}
+.form-control-sm::file-selector-button {
+ padding: 0.25rem 0.5rem;
+ margin: -0.25rem -0.5rem;
+ -webkit-margin-end: 0.5rem;
+ margin-inline-end: 0.5rem;
+}
+.form-control-lg {
+ min-height: calc(
+ 1.5em + 1rem + calc(var(--bs-border-width) * 2)
+ );
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ border-radius: var(--bs-border-radius-lg);
+}
+.form-control-lg::-webkit-file-upload-button {
+ padding: 0.5rem 1rem;
+ margin: -0.5rem -1rem;
+ -webkit-margin-end: 1rem;
+ margin-inline-end: 1rem;
+}
+.form-control-lg::file-selector-button {
+ padding: 0.5rem 1rem;
+ margin: -0.5rem -1rem;
+ -webkit-margin-end: 1rem;
+ margin-inline-end: 1rem;
+}
+textarea.form-control {
+ min-height: calc(
+ 1.5em + 0.75rem + calc(var(--bs-border-width) * 2)
+ );
+}
+textarea.form-control-sm {
+ min-height: calc(
+ 1.5em + 0.5rem + calc(var(--bs-border-width) * 2)
+ );
+}
+textarea.form-control-lg {
+ min-height: calc(
+ 1.5em + 1rem + calc(var(--bs-border-width) * 2)
+ );
+}
+.form-control-color {
+ width: 3rem;
+ height: calc(
+ 1.5em + 0.75rem + calc(var(--bs-border-width) * 2)
+ );
+ padding: 0.375rem;
+}
+.form-control-color:not(:disabled):not([readonly]) {
+ cursor: pointer;
+}
+.form-control-color::-moz-color-swatch {
+ border: 0 !important;
+ border-radius: var(--bs-border-radius);
+}
+.form-control-color::-webkit-color-swatch {
+ border: 0 !important;
+ border-radius: var(--bs-border-radius);
+}
+.form-control-color.form-control-sm {
+ height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
+}
+.form-control-color.form-control-lg {
+ height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));
+}
+.form-select {
+ --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
+ display: block;
+ width: 100%;
+ padding: 0.375rem 2.25rem 0.375rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: var(--bs-body-color);
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: var(--bs-body-bg);
+ background-image: var(--bs-form-select-bg-img),
+ var(--bs-form-select-bg-icon, none);
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ background-size: 16px 12px;
+ border: var(--bs-border-width) solid var(--bs-border-color);
+ border-radius: var(--bs-border-radius);
+ transition: border-color 0.15s ease-in-out,
+ box-shadow 0.15s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .form-select {
+ transition: none;
+ }
+}
+.form-select:focus {
+ border-color: #86b7fe;
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+.form-select[multiple],
+.form-select[size]:not([size="1"]) {
+ padding-right: 0.75rem;
+ background-image: none;
+}
+.form-select:disabled {
+ background-color: var(--bs-secondary-bg);
+}
+.form-select:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 var(--bs-body-color);
+}
+.form-select-sm {
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ padding-left: 0.5rem;
+ font-size: 0.875rem;
+ border-radius: var(--bs-border-radius-sm);
+}
+.form-select-lg {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ padding-left: 1rem;
+ font-size: 1.25rem;
+ border-radius: var(--bs-border-radius-lg);
+}
+[data-bs-theme="dark"] .form-select {
+ --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
+}
+.form-check {
+ display: block;
+ min-height: 1.5rem;
+ padding-left: 1.5em;
+ margin-bottom: 0.125rem;
+}
+.form-check .form-check-input {
+ float: left;
+ margin-left: -1.5em;
+}
+.form-check-reverse {
+ padding-right: 1.5em;
+ padding-left: 0;
+ text-align: right;
+}
+.form-check-reverse .form-check-input {
+ float: right;
+ margin-right: -1.5em;
+ margin-left: 0;
+}
+.form-check-input {
+ --bs-form-check-bg: var(--bs-body-bg);
+ flex-shrink: 0;
+ width: 1em;
+ height: 1em;
+ margin-top: 0.25em;
+ vertical-align: top;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: var(--bs-form-check-bg);
+ background-image: var(--bs-form-check-bg-image);
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ border: var(--bs-border-width) solid var(--bs-border-color);
+ -webkit-print-color-adjust: exact;
+ color-adjust: exact;
+ print-color-adjust: exact;
+}
+.form-check-input[type="checkbox"] {
+ border-radius: 0.25em;
+}
+.form-check-input[type="radio"] {
+ border-radius: 50%;
+}
+.form-check-input:active {
+ filter: brightness(90%);
+}
+.form-check-input:focus {
+ border-color: #86b7fe;
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+.form-check-input:checked {
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+}
+.form-check-input:checked[type="checkbox"] {
+ --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");
+}
+.form-check-input:checked[type="radio"] {
+ --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e");
+}
+.form-check-input[type="checkbox"]:indeterminate {
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+ --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
+}
+.form-check-input:disabled {
+ pointer-events: none;
+ filter: none;
+ opacity: 0.5;
+}
+.form-check-input:disabled ~ .form-check-label,
+.form-check-input[disabled] ~ .form-check-label {
+ cursor: default;
+ opacity: 0.5;
+}
+.form-switch {
+ padding-left: 2.5em;
+}
+.form-switch .form-check-input {
+ --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");
+ width: 2em;
+ margin-left: -2.5em;
+ background-image: var(--bs-form-switch-bg);
+ background-position: left center;
+ border-radius: 2em;
+ transition: background-position 0.15s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .form-switch .form-check-input {
+ transition: none;
+ }
+}
+.form-switch .form-check-input:focus {
+ --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e");
+}
+.form-switch .form-check-input:checked {
+ background-position: right center;
+ --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
+}
+.form-switch.form-check-reverse {
+ padding-right: 2.5em;
+ padding-left: 0;
+}
+.form-switch.form-check-reverse .form-check-input {
+ margin-right: -2.5em;
+ margin-left: 0;
+}
+.form-check-inline {
+ display: inline-block;
+ margin-right: 1rem;
+}
+.btn-check {
+ position: absolute;
+ clip: rect(0, 0, 0, 0);
+ pointer-events: none;
+}
+.btn-check:disabled + .btn,
+.btn-check[disabled] + .btn {
+ pointer-events: none;
+ filter: none;
+ opacity: 0.65;
+}
+[data-bs-theme="dark"]
+ .form-switch
+ .form-check-input:not(:checked):not(:focus) {
+ --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e");
+}
+.form-range {
+ width: 100%;
+ height: 1.5rem;
+ padding: 0;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: transparent;
+}
+.form-range:focus {
+ outline: 0;
+}
+.form-range:focus::-webkit-slider-thumb {
+ box-shadow: 0 0 0 1px #fff,
+ 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+.form-range:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 1px #fff,
+ 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+.form-range::-moz-focus-outer {
+ border: 0;
+}
+.form-range::-webkit-slider-thumb {
+ width: 1rem;
+ height: 1rem;
+ margin-top: -0.25rem;
+ -webkit-appearance: none;
+ appearance: none;
+ background-color: #0d6efd;
+ border: 0;
+ border-radius: 1rem;
+ -webkit-transition: background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .form-range::-webkit-slider-thumb {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+.form-range::-webkit-slider-thumb:active {
+ background-color: #b6d4fe;
+}
+.form-range::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 0.5rem;
+ color: transparent;
+ cursor: pointer;
+ background-color: var(--bs-secondary-bg);
+ border-color: transparent;
+ border-radius: 1rem;
+}
+.form-range::-moz-range-thumb {
+ width: 1rem;
+ height: 1rem;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: #0d6efd;
+ border: 0;
+ border-radius: 1rem;
+ -moz-transition: background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .form-range::-moz-range-thumb {
+ -moz-transition: none;
+ transition: none;
+ }
+}
+.form-range::-moz-range-thumb:active {
+ background-color: #b6d4fe;
+}
+.form-range::-moz-range-track {
+ width: 100%;
+ height: 0.5rem;
+ color: transparent;
+ cursor: pointer;
+ background-color: var(--bs-secondary-bg);
+ border-color: transparent;
+ border-radius: 1rem;
+}
+.form-range:disabled {
+ pointer-events: none;
+}
+.form-range:disabled::-webkit-slider-thumb {
+ background-color: var(--bs-secondary-color);
+}
+.form-range:disabled::-moz-range-thumb {
+ background-color: var(--bs-secondary-color);
+}
+.form-floating {
+ position: relative;
+}
+.form-floating > .form-control,
+.form-floating > .form-control-plaintext,
+.form-floating > .form-select {
+ height: calc(3.5rem + calc(var(--bs-border-width) * 2));
+ min-height: calc(3.5rem + calc(var(--bs-border-width) * 2));
+ line-height: 1.25;
+}
+.form-floating > label {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 2;
+ height: 100%;
+ padding: 1rem 0.75rem;
+ overflow: hidden;
+ text-align: start;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ pointer-events: none;
+ border: var(--bs-border-width) solid transparent;
+ transform-origin: 0 0;
+ transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .form-floating > label {
+ transition: none;
+ }
+}
+.form-floating > .form-control,
+.form-floating > .form-control-plaintext {
+ padding: 1rem 0.75rem;
+}
+.form-floating > .form-control-plaintext::-moz-placeholder,
+.form-floating > .form-control::-moz-placeholder {
+ color: transparent;
+}
+.form-floating > .form-control-plaintext::placeholder,
+.form-floating > .form-control::placeholder {
+ color: transparent;
+}
+.form-floating
+ > .form-control-plaintext:not(:-moz-placeholder-shown),
+.form-floating > .form-control:not(:-moz-placeholder-shown) {
+ padding-top: 1.625rem;
+ padding-bottom: 0.625rem;
+}
+.form-floating > .form-control-plaintext:focus,
+.form-floating > .form-control-plaintext:not(:placeholder-shown),
+.form-floating > .form-control:focus,
+.form-floating > .form-control:not(:placeholder-shown) {
+ padding-top: 1.625rem;
+ padding-bottom: 0.625rem;
+}
+.form-floating > .form-control-plaintext:-webkit-autofill,
+.form-floating > .form-control:-webkit-autofill {
+ padding-top: 1.625rem;
+ padding-bottom: 0.625rem;
+}
+.form-floating > .form-select {
+ padding-top: 1.625rem;
+ padding-bottom: 0.625rem;
+}
+.form-floating
+ > .form-control:not(:-moz-placeholder-shown)
+ ~ label {
+ color: rgba(var(--bs-body-color-rgb), 0.65);
+ transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
+}
+.form-floating > .form-control-plaintext ~ label,
+.form-floating > .form-control:focus ~ label,
+.form-floating > .form-control:not(:placeholder-shown) ~ label,
+.form-floating > .form-select ~ label {
+ color: rgba(var(--bs-body-color-rgb), 0.65);
+ transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
+}
+.form-floating
+ > .form-control:not(:-moz-placeholder-shown)
+ ~ label::after {
+ position: absolute;
+ inset: 1rem 0.375rem;
+ z-index: -1;
+ height: 1.5em;
+ content: "";
+ background-color: var(--bs-body-bg);
+ border-radius: var(--bs-border-radius);
+}
+.form-floating > .form-control-plaintext ~ label::after,
+.form-floating > .form-control:focus ~ label::after,
+.form-floating
+ > .form-control:not(:placeholder-shown)
+ ~ label::after,
+.form-floating > .form-select ~ label::after {
+ position: absolute;
+ inset: 1rem 0.375rem;
+ z-index: -1;
+ height: 1.5em;
+ content: "";
+ background-color: var(--bs-body-bg);
+ border-radius: var(--bs-border-radius);
+}
+.form-floating > .form-control:-webkit-autofill ~ label {
+ color: rgba(var(--bs-body-color-rgb), 0.65);
+ transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
+}
+.form-floating > .form-control-plaintext ~ label {
+ border-width: var(--bs-border-width) 0;
+}
+.form-floating > .form-control:disabled ~ label,
+.form-floating > :disabled ~ label {
+ color: #6c757d;
+}
+.form-floating > .form-control:disabled ~ label::after,
+.form-floating > :disabled ~ label::after {
+ background-color: var(--bs-secondary-bg);
+}
+.input-group {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: stretch;
+ width: 100%;
+}
+.input-group > .form-control,
+.input-group > .form-floating,
+.input-group > .form-select {
+ position: relative;
+ flex: 1 1 auto;
+ width: 1%;
+ min-width: 0;
+}
+.input-group > .form-control:focus,
+.input-group > .form-floating:focus-within,
+.input-group > .form-select:focus {
+ z-index: 5;
+}
+.input-group .btn {
+ position: relative;
+ z-index: 2;
+}
+.input-group .btn:focus {
+ z-index: 5;
+}
+.input-group-text {
+ display: flex;
+ align-items: center;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: var(--bs-body-color);
+ text-align: center;
+ white-space: nowrap;
+ background-color: var(--bs-tertiary-bg);
+ border: var(--bs-border-width) solid var(--bs-border-color);
+ border-radius: var(--bs-border-radius);
+}
+.input-group-lg > .btn,
+.input-group-lg > .form-control,
+.input-group-lg > .form-select,
+.input-group-lg > .input-group-text {
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ border-radius: var(--bs-border-radius-lg);
+}
+.input-group-sm > .btn,
+.input-group-sm > .form-control,
+.input-group-sm > .form-select,
+.input-group-sm > .input-group-text {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ border-radius: var(--bs-border-radius-sm);
+}
+.input-group-lg > .form-select,
+.input-group-sm > .form-select {
+ padding-right: 3rem;
+}
+.input-group:not(.has-validation)
+ > .dropdown-toggle:nth-last-child(n + 3),
+.input-group:not(.has-validation)
+ > .form-floating:not(:last-child)
+ > .form-control,
+.input-group:not(.has-validation)
+ > .form-floating:not(:last-child)
+ > .form-select,
+.input-group:not(.has-validation)
+ > :not(:last-child):not(.dropdown-toggle):not(
+ .dropdown-menu
+ ):not(.form-floating) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+.input-group.has-validation
+ > .dropdown-toggle:nth-last-child(n + 4),
+.input-group.has-validation
+ > .form-floating:nth-last-child(n + 3)
+ > .form-control,
+.input-group.has-validation
+ > .form-floating:nth-last-child(n + 3)
+ > .form-select,
+.input-group.has-validation
+ > :nth-last-child(n + 3):not(.dropdown-toggle):not(
+ .dropdown-menu
+ ):not(.form-floating) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+.input-group
+ > :not(:first-child):not(.dropdown-menu):not(
+ .valid-tooltip
+ ):not(.valid-feedback):not(.invalid-tooltip):not(
+ .invalid-feedback
+ ) {
+ margin-left: calc(var(--bs-border-width) * -1);
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+.input-group > .form-floating:not(:first-child) > .form-control,
+.input-group > .form-floating:not(:first-child) > .form-select {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+.valid-feedback {
+ display: none;
+ width: 100%;
+ margin-top: 0.25rem;
+ font-size: 0.875em;
+ color: var(--bs-form-valid-color);
+}
+.valid-tooltip {
+ position: absolute;
+ top: 100%;
+ z-index: 5;
+ display: none;
+ max-width: 100%;
+ padding: 0.25rem 0.5rem;
+ margin-top: 0.1rem;
+ font-size: 0.875rem;
+ color: #fff;
+ background-color: var(--bs-success);
+ border-radius: var(--bs-border-radius);
+}
+.is-valid ~ .valid-feedback,
+.is-valid ~ .valid-tooltip,
+.was-validated :valid ~ .valid-feedback,
+.was-validated :valid ~ .valid-tooltip {
+ display: block;
+}
+.form-control.is-valid,
+.was-validated .form-control:valid {
+ border-color: var(--bs-form-valid-border-color);
+ padding-right: calc(1.5em + 0.75rem);
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right calc(0.375em + 0.1875rem) center;
+ background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+.form-control.is-valid:focus,
+.was-validated .form-control:valid:focus {
+ border-color: var(--bs-form-valid-border-color);
+ box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);
+}
+.was-validated textarea.form-control:valid,
+textarea.form-control.is-valid {
+ padding-right: calc(1.5em + 0.75rem);
+ background-position: top calc(0.375em + 0.1875rem) right
+ calc(0.375em + 0.1875rem);
+}
+.form-select.is-valid,
+.was-validated .form-select:valid {
+ border-color: var(--bs-form-valid-border-color);
+}
+.form-select.is-valid:not([multiple]):not([size]),
+.form-select.is-valid:not([multiple])[size="1"],
+.was-validated .form-select:valid:not([multiple]):not([size]),
+.was-validated .form-select:valid:not([multiple])[size="1"] {
+ --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
+ padding-right: 4.125rem;
+ background-position: right 0.75rem center, center right 2.25rem;
+ background-size: 16px 12px,
+ calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+.form-select.is-valid:focus,
+.was-validated .form-select:valid:focus {
+ border-color: var(--bs-form-valid-border-color);
+ box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);
+}
+.form-control-color.is-valid,
+.was-validated .form-control-color:valid {
+ width: calc(3rem + calc(1.5em + 0.75rem));
+}
+.form-check-input.is-valid,
+.was-validated .form-check-input:valid {
+ border-color: var(--bs-form-valid-border-color);
+}
+.form-check-input.is-valid:checked,
+.was-validated .form-check-input:valid:checked {
+ background-color: var(--bs-form-valid-color);
+}
+.form-check-input.is-valid:focus,
+.was-validated .form-check-input:valid:focus {
+ box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);
+}
+.form-check-input.is-valid ~ .form-check-label,
+.was-validated .form-check-input:valid ~ .form-check-label {
+ color: var(--bs-form-valid-color);
+}
+.form-check-inline .form-check-input ~ .valid-feedback {
+ margin-left: 0.5em;
+}
+.input-group > .form-control:not(:focus).is-valid,
+.input-group > .form-floating:not(:focus-within).is-valid,
+.input-group > .form-select:not(:focus).is-valid,
+.was-validated .input-group > .form-control:not(:focus):valid,
+.was-validated
+ .input-group
+ > .form-floating:not(:focus-within):valid,
+.was-validated .input-group > .form-select:not(:focus):valid {
+ z-index: 3;
+}
+.invalid-feedback {
+ display: none;
+ width: 100%;
+ margin-top: 0.25rem;
+ font-size: 0.875em;
+ color: var(--bs-form-invalid-color);
+}
+.invalid-tooltip {
+ position: absolute;
+ top: 100%;
+ z-index: 5;
+ display: none;
+ max-width: 100%;
+ padding: 0.25rem 0.5rem;
+ margin-top: 0.1rem;
+ font-size: 0.875rem;
+ color: #fff;
+ background-color: var(--bs-danger);
+ border-radius: var(--bs-border-radius);
+}
+.is-invalid ~ .invalid-feedback,
+.is-invalid ~ .invalid-tooltip,
+.was-validated :invalid ~ .invalid-feedback,
+.was-validated :invalid ~ .invalid-tooltip {
+ display: block;
+}
+.form-control.is-invalid,
+.was-validated .form-control:invalid {
+ border-color: var(--bs-form-invalid-border-color);
+ padding-right: calc(1.5em + 0.75rem);
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right calc(0.375em + 0.1875rem) center;
+ background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+.form-control.is-invalid:focus,
+.was-validated .form-control:invalid:focus {
+ border-color: var(--bs-form-invalid-border-color);
+ box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);
+}
+.was-validated textarea.form-control:invalid,
+textarea.form-control.is-invalid {
+ padding-right: calc(1.5em + 0.75rem);
+ background-position: top calc(0.375em + 0.1875rem) right
+ calc(0.375em + 0.1875rem);
+}
+.form-select.is-invalid,
+.was-validated .form-select:invalid {
+ border-color: var(--bs-form-invalid-border-color);
+}
+.form-select.is-invalid:not([multiple]):not([size]),
+.form-select.is-invalid:not([multiple])[size="1"],
+.was-validated .form-select:invalid:not([multiple]):not([size]),
+.was-validated .form-select:invalid:not([multiple])[size="1"] {
+ --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
+ padding-right: 4.125rem;
+ background-position: right 0.75rem center, center right 2.25rem;
+ background-size: 16px 12px,
+ calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+.form-select.is-invalid:focus,
+.was-validated .form-select:invalid:focus {
+ border-color: var(--bs-form-invalid-border-color);
+ box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);
+}
+.form-control-color.is-invalid,
+.was-validated .form-control-color:invalid {
+ width: calc(3rem + calc(1.5em + 0.75rem));
+}
+.form-check-input.is-invalid,
+.was-validated .form-check-input:invalid {
+ border-color: var(--bs-form-invalid-border-color);
+}
+.form-check-input.is-invalid:checked,
+.was-validated .form-check-input:invalid:checked {
+ background-color: var(--bs-form-invalid-color);
+}
+.form-check-input.is-invalid:focus,
+.was-validated .form-check-input:invalid:focus {
+ box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);
+}
+.form-check-input.is-invalid ~ .form-check-label,
+.was-validated .form-check-input:invalid ~ .form-check-label {
+ color: var(--bs-form-invalid-color);
+}
+.form-check-inline .form-check-input ~ .invalid-feedback {
+ margin-left: 0.5em;
+}
+.input-group > .form-control:not(:focus).is-invalid,
+.input-group > .form-floating:not(:focus-within).is-invalid,
+.input-group > .form-select:not(:focus).is-invalid,
+.was-validated .input-group > .form-control:not(:focus):invalid,
+.was-validated
+ .input-group
+ > .form-floating:not(:focus-within):invalid,
+.was-validated .input-group > .form-select:not(:focus):invalid {
+ z-index: 4;
+}
+.btn {
+ --bs-btn-padding-x: 0.75rem;
+ --bs-btn-padding-y: 0.375rem;
+ --bs-btn-font-family: ;
+ --bs-btn-font-size: 1rem;
+ --bs-btn-font-weight: 400;
+ --bs-btn-line-height: 1.5;
+ --bs-btn-color: var(--bs-body-color);
+ --bs-btn-bg: transparent;
+ --bs-btn-border-width: var(--bs-border-width);
+ --bs-btn-border-color: transparent;
+ --bs-btn-border-radius: var(--bs-border-radius);
+ --bs-btn-hover-border-color: transparent;
+ --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15),
+ 0 1px 1px rgba(0, 0, 0, 0.075);
+ --bs-btn-disabled-opacity: 0.65;
+ --bs-btn-focus-box-shadow: 0 0 0 0.25rem
+ rgba(var(--bs-btn-focus-shadow-rgb), 0.5);
+ display: inline-block;
+ padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
+ font-family: var(--bs-btn-font-family);
+ font-size: var(--bs-btn-font-size);
+ font-weight: var(--bs-btn-font-weight);
+ line-height: var(--bs-btn-line-height);
+ color: var(--bs-btn-color);
+ text-align: center;
+ text-decoration: none;
+ vertical-align: middle;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ border: var(--bs-btn-border-width) solid
+ var(--bs-btn-border-color);
+ border-radius: var(--bs-btn-border-radius);
+ background-color: var(--bs-btn-bg);
+ transition: color 0.15s ease-in-out,
+ background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .btn {
+ transition: none;
+ }
+}
+.btn:hover {
+ color: var(--bs-btn-hover-color);
+ background-color: var(--bs-btn-hover-bg);
+ border-color: var(--bs-btn-hover-border-color);
+}
+.btn-check + .btn:hover {
+ color: var(--bs-btn-color);
+ background-color: var(--bs-btn-bg);
+ border-color: var(--bs-btn-border-color);
+}
+.btn:focus-visible {
+ color: var(--bs-btn-hover-color);
+ background-color: var(--bs-btn-hover-bg);
+ border-color: var(--bs-btn-hover-border-color);
+ outline: 0;
+ box-shadow: var(--bs-btn-focus-box-shadow);
+}
+.btn-check:focus-visible + .btn {
+ border-color: var(--bs-btn-hover-border-color);
+ outline: 0;
+ box-shadow: var(--bs-btn-focus-box-shadow);
+}
+.btn-check:checked + .btn,
+.btn.active,
+.btn.show,
+.btn:first-child:active,
+:not(.btn-check) + .btn:active {
+ color: var(--bs-btn-active-color);
+ background-color: var(--bs-btn-active-bg);
+ border-color: var(--bs-btn-active-border-color);
+}
+.btn-check:checked + .btn:focus-visible,
+.btn.active:focus-visible,
+.btn.show:focus-visible,
+.btn:first-child:active:focus-visible,
+:not(.btn-check) + .btn:active:focus-visible {
+ box-shadow: var(--bs-btn-focus-box-shadow);
+}
+.btn.disabled,
+.btn:disabled,
+fieldset:disabled .btn {
+ color: var(--bs-btn-disabled-color);
+ pointer-events: none;
+ background-color: var(--bs-btn-disabled-bg);
+ border-color: var(--bs-btn-disabled-border-color);
+ opacity: var(--bs-btn-disabled-opacity);
+}
+.btn-primary {
+ --bs-btn-color: #fff;
+ --bs-btn-bg: #0d6efd;
+ --bs-btn-border-color: #0d6efd;
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #0b5ed7;
+ --bs-btn-hover-border-color: #0a58ca;
+ --bs-btn-focus-shadow-rgb: 49, 132, 253;
+ --bs-btn-active-color: #fff;
+ --bs-btn-active-bg: #0a58ca;
+ --bs-btn-active-border-color: #0a53be;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #fff;
+ --bs-btn-disabled-bg: #0d6efd;
+ --bs-btn-disabled-border-color: #0d6efd;
+}
+.btn-secondary {
+ --bs-btn-color: #fff;
+ --bs-btn-bg: #6c757d;
+ --bs-btn-border-color: #6c757d;
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #5c636a;
+ --bs-btn-hover-border-color: #565e64;
+ --bs-btn-focus-shadow-rgb: 130, 138, 145;
+ --bs-btn-active-color: #fff;
+ --bs-btn-active-bg: #565e64;
+ --bs-btn-active-border-color: #51585e;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #fff;
+ --bs-btn-disabled-bg: #6c757d;
+ --bs-btn-disabled-border-color: #6c757d;
+}
+.btn-success {
+ --bs-btn-color: #fff;
+ --bs-btn-bg: #198754;
+ --bs-btn-border-color: #198754;
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #157347;
+ --bs-btn-hover-border-color: #146c43;
+ --bs-btn-focus-shadow-rgb: 60, 153, 110;
+ --bs-btn-active-color: #fff;
+ --bs-btn-active-bg: #146c43;
+ --bs-btn-active-border-color: #13653f;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #fff;
+ --bs-btn-disabled-bg: #198754;
+ --bs-btn-disabled-border-color: #198754;
+}
+.btn-info {
+ --bs-btn-color: #000;
+ --bs-btn-bg: #0dcaf0;
+ --bs-btn-border-color: #0dcaf0;
+ --bs-btn-hover-color: #000;
+ --bs-btn-hover-bg: #31d2f2;
+ --bs-btn-hover-border-color: #25cff2;
+ --bs-btn-focus-shadow-rgb: 11, 172, 204;
+ --bs-btn-active-color: #000;
+ --bs-btn-active-bg: #3dd5f3;
+ --bs-btn-active-border-color: #25cff2;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #000;
+ --bs-btn-disabled-bg: #0dcaf0;
+ --bs-btn-disabled-border-color: #0dcaf0;
+}
+.btn-warning {
+ --bs-btn-color: #000;
+ --bs-btn-bg: #ffc107;
+ --bs-btn-border-color: #ffc107;
+ --bs-btn-hover-color: #000;
+ --bs-btn-hover-bg: #ffca2c;
+ --bs-btn-hover-border-color: #ffc720;
+ --bs-btn-focus-shadow-rgb: 217, 164, 6;
+ --bs-btn-active-color: #000;
+ --bs-btn-active-bg: #ffcd39;
+ --bs-btn-active-border-color: #ffc720;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #000;
+ --bs-btn-disabled-bg: #ffc107;
+ --bs-btn-disabled-border-color: #ffc107;
+}
+.btn-danger {
+ --bs-btn-color: #fff;
+ --bs-btn-bg: #dc3545;
+ --bs-btn-border-color: #dc3545;
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #bb2d3b;
+ --bs-btn-hover-border-color: #b02a37;
+ --bs-btn-focus-shadow-rgb: 225, 83, 97;
+ --bs-btn-active-color: #fff;
+ --bs-btn-active-bg: #b02a37;
+ --bs-btn-active-border-color: #a52834;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #fff;
+ --bs-btn-disabled-bg: #dc3545;
+ --bs-btn-disabled-border-color: #dc3545;
+}
+.btn-light {
+ --bs-btn-color: #000;
+ --bs-btn-bg: #f8f9fa;
+ --bs-btn-border-color: #f8f9fa;
+ --bs-btn-hover-color: #000;
+ --bs-btn-hover-bg: #d3d4d5;
+ --bs-btn-hover-border-color: #c6c7c8;
+ --bs-btn-focus-shadow-rgb: 211, 212, 213;
+ --bs-btn-active-color: #000;
+ --bs-btn-active-bg: #c6c7c8;
+ --bs-btn-active-border-color: #babbbc;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #000;
+ --bs-btn-disabled-bg: #f8f9fa;
+ --bs-btn-disabled-border-color: #f8f9fa;
+}
+.btn-dark {
+ --bs-btn-color: #fff;
+ --bs-btn-bg: #212529;
+ --bs-btn-border-color: #212529;
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #424649;
+ --bs-btn-hover-border-color: #373b3e;
+ --bs-btn-focus-shadow-rgb: 66, 70, 73;
+ --bs-btn-active-color: #fff;
+ --bs-btn-active-bg: #4d5154;
+ --bs-btn-active-border-color: #373b3e;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #fff;
+ --bs-btn-disabled-bg: #212529;
+ --bs-btn-disabled-border-color: #212529;
+}
+.btn-outline-primary {
+ --bs-btn-color: #0d6efd;
+ --bs-btn-border-color: #0d6efd;
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #0d6efd;
+ --bs-btn-hover-border-color: #0d6efd;
+ --bs-btn-focus-shadow-rgb: 13, 110, 253;
+ --bs-btn-active-color: #fff;
+ --bs-btn-active-bg: #0d6efd;
+ --bs-btn-active-border-color: #0d6efd;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #0d6efd;
+ --bs-btn-disabled-bg: transparent;
+ --bs-btn-disabled-border-color: #0d6efd;
+ --bs-gradient: none;
+}
+.btn-outline-secondary {
+ --bs-btn-color: #6c757d;
+ --bs-btn-border-color: #6c757d;
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #6c757d;
+ --bs-btn-hover-border-color: #6c757d;
+ --bs-btn-focus-shadow-rgb: 108, 117, 125;
+ --bs-btn-active-color: #fff;
+ --bs-btn-active-bg: #6c757d;
+ --bs-btn-active-border-color: #6c757d;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #6c757d;
+ --bs-btn-disabled-bg: transparent;
+ --bs-btn-disabled-border-color: #6c757d;
+ --bs-gradient: none;
+}
+.btn-outline-success {
+ --bs-btn-color: #198754;
+ --bs-btn-border-color: #198754;
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #198754;
+ --bs-btn-hover-border-color: #198754;
+ --bs-btn-focus-shadow-rgb: 25, 135, 84;
+ --bs-btn-active-color: #fff;
+ --bs-btn-active-bg: #198754;
+ --bs-btn-active-border-color: #198754;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #198754;
+ --bs-btn-disabled-bg: transparent;
+ --bs-btn-disabled-border-color: #198754;
+ --bs-gradient: none;
+}
+.btn-outline-info {
+ --bs-btn-color: #0dcaf0;
+ --bs-btn-border-color: #0dcaf0;
+ --bs-btn-hover-color: #000;
+ --bs-btn-hover-bg: #0dcaf0;
+ --bs-btn-hover-border-color: #0dcaf0;
+ --bs-btn-focus-shadow-rgb: 13, 202, 240;
+ --bs-btn-active-color: #000;
+ --bs-btn-active-bg: #0dcaf0;
+ --bs-btn-active-border-color: #0dcaf0;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #0dcaf0;
+ --bs-btn-disabled-bg: transparent;
+ --bs-btn-disabled-border-color: #0dcaf0;
+ --bs-gradient: none;
+}
+.btn-outline-warning {
+ --bs-btn-color: #ffc107;
+ --bs-btn-border-color: #ffc107;
+ --bs-btn-hover-color: #000;
+ --bs-btn-hover-bg: #ffc107;
+ --bs-btn-hover-border-color: #ffc107;
+ --bs-btn-focus-shadow-rgb: 255, 193, 7;
+ --bs-btn-active-color: #000;
+ --bs-btn-active-bg: #ffc107;
+ --bs-btn-active-border-color: #ffc107;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #ffc107;
+ --bs-btn-disabled-bg: transparent;
+ --bs-btn-disabled-border-color: #ffc107;
+ --bs-gradient: none;
+}
+.btn-outline-danger {
+ --bs-btn-color: #dc3545;
+ --bs-btn-border-color: #dc3545;
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #dc3545;
+ --bs-btn-hover-border-color: #dc3545;
+ --bs-btn-focus-shadow-rgb: 220, 53, 69;
+ --bs-btn-active-color: #fff;
+ --bs-btn-active-bg: #dc3545;
+ --bs-btn-active-border-color: #dc3545;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #dc3545;
+ --bs-btn-disabled-bg: transparent;
+ --bs-btn-disabled-border-color: #dc3545;
+ --bs-gradient: none;
+}
+.btn-outline-light {
+ --bs-btn-color: #f8f9fa;
+ --bs-btn-border-color: #f8f9fa;
+ --bs-btn-hover-color: #000;
+ --bs-btn-hover-bg: #f8f9fa;
+ --bs-btn-hover-border-color: #f8f9fa;
+ --bs-btn-focus-shadow-rgb: 248, 249, 250;
+ --bs-btn-active-color: #000;
+ --bs-btn-active-bg: #f8f9fa;
+ --bs-btn-active-border-color: #f8f9fa;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #f8f9fa;
+ --bs-btn-disabled-bg: transparent;
+ --bs-btn-disabled-border-color: #f8f9fa;
+ --bs-gradient: none;
+}
+.btn-outline-dark {
+ --bs-btn-color: #212529;
+ --bs-btn-border-color: #212529;
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #212529;
+ --bs-btn-hover-border-color: #212529;
+ --bs-btn-focus-shadow-rgb: 33, 37, 41;
+ --bs-btn-active-color: #fff;
+ --bs-btn-active-bg: #212529;
+ --bs-btn-active-border-color: #212529;
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --bs-btn-disabled-color: #212529;
+ --bs-btn-disabled-bg: transparent;
+ --bs-btn-disabled-border-color: #212529;
+ --bs-gradient: none;
+}
+.btn-link {
+ --bs-btn-font-weight: 400;
+ --bs-btn-color: var(--bs-link-color);
+ --bs-btn-bg: transparent;
+ --bs-btn-border-color: transparent;
+ --bs-btn-hover-color: var(--bs-link-hover-color);
+ --bs-btn-hover-border-color: transparent;
+ --bs-btn-active-color: var(--bs-link-hover-color);
+ --bs-btn-active-border-color: transparent;
+ --bs-btn-disabled-color: #6c757d;
+ --bs-btn-disabled-border-color: transparent;
+ --bs-btn-box-shadow: 0 0 0 #000;
+ --bs-btn-focus-shadow-rgb: 49, 132, 253;
+ text-decoration: underline;
+}
+.btn-link:focus-visible {
+ color: var(--bs-btn-color);
+}
+.btn-link:hover {
+ color: var(--bs-btn-hover-color);
+}
+.btn-group-lg > .btn,
+.btn-lg {
+ --bs-btn-padding-y: 0.5rem;
+ --bs-btn-padding-x: 1rem;
+ --bs-btn-font-size: 1.25rem;
+ --bs-btn-border-radius: var(--bs-border-radius-lg);
+}
+.btn-group-sm > .btn,
+.btn-sm {
+ --bs-btn-padding-y: 0.25rem;
+ --bs-btn-padding-x: 0.5rem;
+ --bs-btn-font-size: 0.875rem;
+ --bs-btn-border-radius: var(--bs-border-radius-sm);
+}
+.fade {
+ transition: opacity 0.15s linear;
+}
+@media (prefers-reduced-motion: reduce) {
+ .fade {
+ transition: none;
+ }
+}
+.fade:not(.show) {
+ opacity: 0;
+}
+.collapse:not(.show) {
+ display: none;
+}
+.collapsing {
+ height: 0;
+ overflow: hidden;
+ transition: height 0.35s ease;
+}
+@media (prefers-reduced-motion: reduce) {
+ .collapsing {
+ transition: none;
+ }
+}
+.collapsing.collapse-horizontal {
+ width: 0;
+ height: auto;
+ transition: width 0.35s ease;
+}
+@media (prefers-reduced-motion: reduce) {
+ .collapsing.collapse-horizontal {
+ transition: none;
+ }
+}
+.dropdown,
+.dropdown-center,
+.dropend,
+.dropstart,
+.dropup,
+.dropup-center {
+ position: relative;
+}
+.dropdown-toggle {
+ white-space: nowrap;
+}
+.dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0;
+ border-left: 0.3em solid transparent;
+}
+.dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+.dropdown-menu {
+ --bs-dropdown-zindex: 1000;
+ --bs-dropdown-min-width: 10rem;
+ --bs-dropdown-padding-x: 0;
+ --bs-dropdown-padding-y: 0.5rem;
+ --bs-dropdown-spacer: 0.125rem;
+ --bs-dropdown-font-size: 1rem;
+ --bs-dropdown-color: var(--bs-body-color);
+ --bs-dropdown-bg: var(--bs-body-bg);
+ --bs-dropdown-border-color: var(--bs-border-color-translucent);
+ --bs-dropdown-border-radius: var(--bs-border-radius);
+ --bs-dropdown-border-width: var(--bs-border-width);
+ --bs-dropdown-inner-border-radius: calc(
+ var(--bs-border-radius) - var(--bs-border-width)
+ );
+ --bs-dropdown-divider-bg: var(--bs-border-color-translucent);
+ --bs-dropdown-divider-margin-y: 0.5rem;
+ --bs-dropdown-box-shadow: var(--bs-box-shadow);
+ --bs-dropdown-link-color: var(--bs-body-color);
+ --bs-dropdown-link-hover-color: var(--bs-body-color);
+ --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);
+ --bs-dropdown-link-active-color: #fff;
+ --bs-dropdown-link-active-bg: #0d6efd;
+ --bs-dropdown-link-disabled-color: var(--bs-tertiary-color);
+ --bs-dropdown-item-padding-x: 1rem;
+ --bs-dropdown-item-padding-y: 0.25rem;
+ --bs-dropdown-header-color: #6c757d;
+ --bs-dropdown-header-padding-x: 1rem;
+ --bs-dropdown-header-padding-y: 0.5rem;
+ position: absolute;
+ z-index: var(--bs-dropdown-zindex);
+ display: none;
+ min-width: var(--bs-dropdown-min-width);
+ padding: var(--bs-dropdown-padding-y)
+ var(--bs-dropdown-padding-x);
+ margin: 0;
+ font-size: var(--bs-dropdown-font-size);
+ color: var(--bs-dropdown-color);
+ text-align: left;
+ list-style: none;
+ background-color: var(--bs-dropdown-bg);
+ background-clip: padding-box;
+ border: var(--bs-dropdown-border-width) solid
+ var(--bs-dropdown-border-color);
+ border-radius: var(--bs-dropdown-border-radius);
+}
+.dropdown-menu[data-bs-popper] {
+ top: 100%;
+ left: 0;
+ margin-top: var(--bs-dropdown-spacer);
+}
+.dropdown-menu-start {
+ --bs-position: start;
+}
+.dropdown-menu-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+}
+.dropdown-menu-end {
+ --bs-position: end;
+}
+.dropdown-menu-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+}
+@media (min-width: 576px) {
+ .dropdown-menu-sm-start {
+ --bs-position: start;
+ }
+ .dropdown-menu-sm-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+ }
+ .dropdown-menu-sm-end {
+ --bs-position: end;
+ }
+ .dropdown-menu-sm-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+ }
+}
+@media (min-width: 768px) {
+ .dropdown-menu-md-start {
+ --bs-position: start;
+ }
+ .dropdown-menu-md-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+ }
+ .dropdown-menu-md-end {
+ --bs-position: end;
+ }
+ .dropdown-menu-md-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+ }
+}
+@media (min-width: 992px) {
+ .dropdown-menu-lg-start {
+ --bs-position: start;
+ }
+ .dropdown-menu-lg-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+ }
+ .dropdown-menu-lg-end {
+ --bs-position: end;
+ }
+ .dropdown-menu-lg-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+ }
+}
+@media (min-width: 1200px) {
+ .dropdown-menu-xl-start {
+ --bs-position: start;
+ }
+ .dropdown-menu-xl-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+ }
+ .dropdown-menu-xl-end {
+ --bs-position: end;
+ }
+ .dropdown-menu-xl-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+ }
+}
+@media (min-width: 1400px) {
+ .dropdown-menu-xxl-start {
+ --bs-position: start;
+ }
+ .dropdown-menu-xxl-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+ }
+ .dropdown-menu-xxl-end {
+ --bs-position: end;
+ }
+ .dropdown-menu-xxl-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+ }
+}
+.dropup .dropdown-menu[data-bs-popper] {
+ top: auto;
+ bottom: 100%;
+ margin-top: 0;
+ margin-bottom: var(--bs-dropdown-spacer);
+}
+.dropup .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0.3em solid;
+ border-left: 0.3em solid transparent;
+}
+.dropup .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+.dropend .dropdown-menu[data-bs-popper] {
+ top: 0;
+ right: auto;
+ left: 100%;
+ margin-top: 0;
+ margin-left: var(--bs-dropdown-spacer);
+}
+.dropend .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid transparent;
+ border-right: 0;
+ border-bottom: 0.3em solid transparent;
+ border-left: 0.3em solid;
+}
+.dropend .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+.dropend .dropdown-toggle::after {
+ vertical-align: 0;
+}
+.dropstart .dropdown-menu[data-bs-popper] {
+ top: 0;
+ right: 100%;
+ left: auto;
+ margin-top: 0;
+ margin-right: var(--bs-dropdown-spacer);
+}
+.dropstart .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+}
+.dropstart .dropdown-toggle::after {
+ display: none;
+}
+.dropstart .dropdown-toggle::before {
+ display: inline-block;
+ margin-right: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid transparent;
+ border-right: 0.3em solid;
+ border-bottom: 0.3em solid transparent;
+}
+.dropstart .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+.dropstart .dropdown-toggle::before {
+ vertical-align: 0;
+}
+.dropdown-divider {
+ height: 0;
+ margin: var(--bs-dropdown-divider-margin-y) 0;
+ overflow: hidden;
+ border-top: 1px solid var(--bs-dropdown-divider-bg);
+ opacity: 1;
+}
+.dropdown-item {
+ display: block;
+ width: 100%;
+ padding: var(--bs-dropdown-item-padding-y)
+ var(--bs-dropdown-item-padding-x);
+ clear: both;
+ font-weight: 400;
+ color: var(--bs-dropdown-link-color);
+ text-align: inherit;
+ text-decoration: none;
+ white-space: nowrap;
+ background-color: transparent;
+ border: 0;
+ border-radius: var(--bs-dropdown-item-border-radius, 0);
+}
+.dropdown-item:focus,
+.dropdown-item:hover {
+ color: var(--bs-dropdown-link-hover-color);
+ background-color: var(--bs-dropdown-link-hover-bg);
+}
+.dropdown-item.active,
+.dropdown-item:active {
+ color: var(--bs-dropdown-link-active-color);
+ text-decoration: none;
+ background-color: var(--bs-dropdown-link-active-bg);
+}
+.dropdown-item.disabled,
+.dropdown-item:disabled {
+ color: var(--bs-dropdown-link-disabled-color);
+ pointer-events: none;
+ background-color: transparent;
+}
+.dropdown-menu.show {
+ display: block;
+}
+.dropdown-header {
+ display: block;
+ padding: var(--bs-dropdown-header-padding-y)
+ var(--bs-dropdown-header-padding-x);
+ margin-bottom: 0;
+ font-size: 0.875rem;
+ color: var(--bs-dropdown-header-color);
+ white-space: nowrap;
+}
+.dropdown-item-text {
+ display: block;
+ padding: var(--bs-dropdown-item-padding-y)
+ var(--bs-dropdown-item-padding-x);
+ color: var(--bs-dropdown-link-color);
+}
+.dropdown-menu-dark {
+ --bs-dropdown-color: #dee2e6;
+ --bs-dropdown-bg: #343a40;
+ --bs-dropdown-border-color: var(--bs-border-color-translucent);
+ --bs-dropdown-box-shadow: ;
+ --bs-dropdown-link-color: #dee2e6;
+ --bs-dropdown-link-hover-color: #fff;
+ --bs-dropdown-divider-bg: var(--bs-border-color-translucent);
+ --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);
+ --bs-dropdown-link-active-color: #fff;
+ --bs-dropdown-link-active-bg: #0d6efd;
+ --bs-dropdown-link-disabled-color: #adb5bd;
+ --bs-dropdown-header-color: #adb5bd;
+}
+.btn-group,
+.btn-group-vertical {
+ position: relative;
+ display: inline-flex;
+ vertical-align: middle;
+}
+.btn-group-vertical > .btn,
+.btn-group > .btn {
+ position: relative;
+ flex: 1 1 auto;
+}
+.btn-group-vertical > .btn-check:checked + .btn,
+.btn-group-vertical > .btn-check:focus + .btn,
+.btn-group-vertical > .btn.active,
+.btn-group-vertical > .btn:active,
+.btn-group-vertical > .btn:focus,
+.btn-group-vertical > .btn:hover,
+.btn-group > .btn-check:checked + .btn,
+.btn-group > .btn-check:focus + .btn,
+.btn-group > .btn.active,
+.btn-group > .btn:active,
+.btn-group > .btn:focus,
+.btn-group > .btn:hover {
+ z-index: 1;
+}
+.btn-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+}
+.btn-toolbar .input-group {
+ width: auto;
+}
+.btn-group {
+ border-radius: var(--bs-border-radius);
+}
+.btn-group > .btn-group:not(:first-child),
+.btn-group > :not(.btn-check:first-child) + .btn {
+ margin-left: calc(var(--bs-border-width) * -1);
+}
+.btn-group > .btn-group:not(:last-child) > .btn,
+.btn-group > .btn.dropdown-toggle-split:first-child,
+.btn-group > .btn:not(:last-child):not(.dropdown-toggle) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+.btn-group > .btn-group:not(:first-child) > .btn,
+.btn-group > .btn:nth-child(n + 3),
+.btn-group > :not(.btn-check) + .btn {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+.dropdown-toggle-split {
+ padding-right: 0.5625rem;
+ padding-left: 0.5625rem;
+}
+.dropdown-toggle-split::after,
+.dropend .dropdown-toggle-split::after,
+.dropup .dropdown-toggle-split::after {
+ margin-left: 0;
+}
+.dropstart .dropdown-toggle-split::before {
+ margin-right: 0;
+}
+.btn-group-sm > .btn + .dropdown-toggle-split,
+.btn-sm + .dropdown-toggle-split {
+ padding-right: 0.375rem;
+ padding-left: 0.375rem;
+}
+.btn-group-lg > .btn + .dropdown-toggle-split,
+.btn-lg + .dropdown-toggle-split {
+ padding-right: 0.75rem;
+ padding-left: 0.75rem;
+}
+.btn-group-vertical {
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+}
+.btn-group-vertical > .btn,
+.btn-group-vertical > .btn-group {
+ width: 100%;
+}
+.btn-group-vertical > .btn-group:not(:first-child),
+.btn-group-vertical > .btn:not(:first-child) {
+ margin-top: calc(var(--bs-border-width) * -1);
+}
+.btn-group-vertical > .btn-group:not(:last-child) > .btn,
+.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle) {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
+.btn-group-vertical > .btn-group:not(:first-child) > .btn,
+.btn-group-vertical > .btn ~ .btn {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+.nav {
+ --bs-nav-link-padding-x: 1rem;
+ --bs-nav-link-padding-y: 0.5rem;
+ --bs-nav-link-font-weight: ;
+ --bs-nav-link-color: var(--bs-link-color);
+ --bs-nav-link-hover-color: var(--bs-link-hover-color);
+ --bs-nav-link-disabled-color: var(--bs-secondary-color);
+ display: flex;
+ flex-wrap: wrap;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+.nav-link {
+ display: block;
+ padding: var(--bs-nav-link-padding-y)
+ var(--bs-nav-link-padding-x);
+ font-size: var(--bs-nav-link-font-size);
+ font-weight: var(--bs-nav-link-font-weight);
+ color: var(--bs-nav-link-color);
+ text-decoration: none;
+ background: 0 0;
+ border: 0;
+ transition: color 0.15s ease-in-out,
+ background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .nav-link {
+ transition: none;
+ }
+}
+.nav-link:focus,
+.nav-link:hover {
+ color: var(--bs-nav-link-hover-color);
+}
+.nav-link:focus-visible {
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+.nav-link.disabled,
+.nav-link:disabled {
+ color: var(--bs-nav-link-disabled-color);
+ pointer-events: none;
+ cursor: default;
+}
+.nav-tabs {
+ --bs-nav-tabs-border-width: var(--bs-border-width);
+ --bs-nav-tabs-border-color: var(--bs-border-color);
+ --bs-nav-tabs-border-radius: var(--bs-border-radius);
+ --bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg)
+ var(--bs-secondary-bg) var(--bs-border-color);
+ --bs-nav-tabs-link-active-color: var(--bs-emphasis-color);
+ --bs-nav-tabs-link-active-bg: var(--bs-body-bg);
+ --bs-nav-tabs-link-active-border-color: var(--bs-border-color)
+ var(--bs-border-color) var(--bs-body-bg);
+ border-bottom: var(--bs-nav-tabs-border-width) solid
+ var(--bs-nav-tabs-border-color);
+}
+.nav-tabs .nav-link {
+ margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width));
+ border: var(--bs-nav-tabs-border-width) solid transparent;
+ border-top-left-radius: var(--bs-nav-tabs-border-radius);
+ border-top-right-radius: var(--bs-nav-tabs-border-radius);
+}
+.nav-tabs .nav-link:focus,
+.nav-tabs .nav-link:hover {
+ isolation: isolate;
+ border-color: var(--bs-nav-tabs-link-hover-border-color);
+}
+.nav-tabs .nav-item.show .nav-link,
+.nav-tabs .nav-link.active {
+ color: var(--bs-nav-tabs-link-active-color);
+ background-color: var(--bs-nav-tabs-link-active-bg);
+ border-color: var(--bs-nav-tabs-link-active-border-color);
+}
+.nav-tabs .dropdown-menu {
+ margin-top: calc(-1 * var(--bs-nav-tabs-border-width));
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+.nav-pills {
+ --bs-nav-pills-border-radius: var(--bs-border-radius);
+ --bs-nav-pills-link-active-color: #fff;
+ --bs-nav-pills-link-active-bg: #0d6efd;
+}
+.nav-pills .nav-link {
+ border-radius: var(--bs-nav-pills-border-radius);
+}
+.nav-pills .nav-link.active,
+.nav-pills .show > .nav-link {
+ color: var(--bs-nav-pills-link-active-color);
+ background-color: var(--bs-nav-pills-link-active-bg);
+}
+.nav-underline {
+ --bs-nav-underline-gap: 1rem;
+ --bs-nav-underline-border-width: 0.125rem;
+ --bs-nav-underline-link-active-color: var(--bs-emphasis-color);
+ gap: var(--bs-nav-underline-gap);
+}
+.nav-underline .nav-link {
+ padding-right: 0;
+ padding-left: 0;
+ border-bottom: var(--bs-nav-underline-border-width) solid
+ transparent;
+}
+.nav-underline .nav-link:focus,
+.nav-underline .nav-link:hover {
+ border-bottom-color: currentcolor;
+}
+.nav-underline .nav-link.active,
+.nav-underline .show > .nav-link {
+ font-weight: 700;
+ color: var(--bs-nav-underline-link-active-color);
+ border-bottom-color: currentcolor;
+}
+.nav-fill .nav-item,
+.nav-fill > .nav-link {
+ flex: 1 1 auto;
+ text-align: center;
+}
+.nav-justified .nav-item,
+.nav-justified > .nav-link {
+ flex-basis: 0;
+ flex-grow: 1;
+ text-align: center;
+}
+.nav-fill .nav-item .nav-link,
+.nav-justified .nav-item .nav-link {
+ width: 100%;
+}
+.tab-content > .tab-pane {
+ display: none;
+}
+.tab-content > .active {
+ display: block;
+}
+.navbar {
+ --bs-navbar-padding-x: 0;
+ --bs-navbar-padding-y: 0.5rem;
+ --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65);
+ --bs-navbar-hover-color: rgba(
+ var(--bs-emphasis-color-rgb),
+ 0.8
+ );
+ --bs-navbar-disabled-color: rgba(
+ var(--bs-emphasis-color-rgb),
+ 0.3
+ );
+ --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);
+ --bs-navbar-brand-padding-y: 0.3125rem;
+ --bs-navbar-brand-margin-end: 1rem;
+ --bs-navbar-brand-font-size: 1.25rem;
+ --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);
+ --bs-navbar-brand-hover-color: rgba(
+ var(--bs-emphasis-color-rgb),
+ 1
+ );
+ --bs-navbar-nav-link-padding-x: 0.5rem;
+ --bs-navbar-toggler-padding-y: 0.25rem;
+ --bs-navbar-toggler-padding-x: 0.75rem;
+ --bs-navbar-toggler-font-size: 1.25rem;
+ --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+ --bs-navbar-toggler-border-color: rgba(
+ var(--bs-emphasis-color-rgb),
+ 0.15
+ );
+ --bs-navbar-toggler-border-radius: var(--bs-border-radius);
+ --bs-navbar-toggler-focus-width: 0.25rem;
+ --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);
+}
+.navbar > .container,
+.navbar > .container-fluid,
+.navbar > .container-lg,
+.navbar > .container-md,
+.navbar > .container-sm,
+.navbar > .container-xl,
+.navbar > .container-xxl {
+ display: flex;
+ flex-wrap: inherit;
+ align-items: center;
+ justify-content: space-between;
+}
+.navbar-brand {
+ padding-top: var(--bs-navbar-brand-padding-y);
+ padding-bottom: var(--bs-navbar-brand-padding-y);
+ margin-right: var(--bs-navbar-brand-margin-end);
+ font-size: var(--bs-navbar-brand-font-size);
+ color: var(--bs-navbar-brand-color);
+ text-decoration: none;
+ white-space: nowrap;
+}
+.navbar-brand:focus,
+.navbar-brand:hover {
+ color: var(--bs-navbar-brand-hover-color);
+}
+.navbar-nav {
+ --bs-nav-link-padding-x: 0;
+ --bs-nav-link-padding-y: 0.5rem;
+ --bs-nav-link-font-weight: ;
+ --bs-nav-link-color: var(--bs-navbar-color);
+ --bs-nav-link-hover-color: var(--bs-navbar-hover-color);
+ --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);
+ display: flex;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+.navbar-nav .nav-link.active,
+.navbar-nav .nav-link.show {
+ color: var(--bs-navbar-active-color);
+}
+.navbar-nav .dropdown-menu {
+ position: static;
+}
+.navbar-text {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ color: var(--bs-navbar-color);
+}
+.navbar-text a,
+.navbar-text a:focus,
+.navbar-text a:hover {
+ color: var(--bs-navbar-active-color);
+}
+.navbar-collapse {
+ flex-basis: 100%;
+ flex-grow: 1;
+ align-items: center;
+}
+.navbar-toggler {
+ padding: var(--bs-navbar-toggler-padding-y)
+ var(--bs-navbar-toggler-padding-x);
+ font-size: var(--bs-navbar-toggler-font-size);
+ line-height: 1;
+ color: var(--bs-navbar-color);
+ background-color: transparent;
+ border: var(--bs-border-width) solid
+ var(--bs-navbar-toggler-border-color);
+ border-radius: var(--bs-navbar-toggler-border-radius);
+ transition: var(--bs-navbar-toggler-transition);
+}
+@media (prefers-reduced-motion: reduce) {
+ .navbar-toggler {
+ transition: none;
+ }
+}
+.navbar-toggler:hover {
+ text-decoration: none;
+}
+.navbar-toggler:focus {
+ text-decoration: none;
+ outline: 0;
+ box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width);
+}
+.navbar-toggler-icon {
+ display: inline-block;
+ width: 1.5em;
+ height: 1.5em;
+ vertical-align: middle;
+ background-image: var(--bs-navbar-toggler-icon-bg);
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 100%;
+}
+.navbar-nav-scroll {
+ max-height: var(--bs-scroll-height, 75vh);
+ overflow-y: auto;
+}
+@media (min-width: 576px) {
+ .navbar-expand-sm {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+ .navbar-expand-sm .navbar-nav {
+ flex-direction: row;
+ }
+ .navbar-expand-sm .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ .navbar-expand-sm .navbar-nav .nav-link {
+ padding-right: var(--bs-navbar-nav-link-padding-x);
+ padding-left: var(--bs-navbar-nav-link-padding-x);
+ }
+ .navbar-expand-sm .navbar-nav-scroll {
+ overflow: visible;
+ }
+ .navbar-expand-sm .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+ .navbar-expand-sm .navbar-toggler {
+ display: none;
+ }
+ .navbar-expand-sm .offcanvas {
+ position: static;
+ z-index: auto;
+ flex-grow: 1;
+ width: auto !important;
+ height: auto !important;
+ visibility: visible !important;
+ background-color: transparent !important;
+ border: 0 !important;
+ transform: none !important;
+ transition: none;
+ }
+ .navbar-expand-sm .offcanvas .offcanvas-header {
+ display: none;
+ }
+ .navbar-expand-sm .offcanvas .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ }
+}
+@media (min-width: 768px) {
+ .navbar-expand-md {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+ .navbar-expand-md .navbar-nav {
+ flex-direction: row;
+ }
+ .navbar-expand-md .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ .navbar-expand-md .navbar-nav .nav-link {
+ padding-right: var(--bs-navbar-nav-link-padding-x);
+ padding-left: var(--bs-navbar-nav-link-padding-x);
+ }
+ .navbar-expand-md .navbar-nav-scroll {
+ overflow: visible;
+ }
+ .navbar-expand-md .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+ .navbar-expand-md .navbar-toggler {
+ display: none;
+ }
+ .navbar-expand-md .offcanvas {
+ position: static;
+ z-index: auto;
+ flex-grow: 1;
+ width: auto !important;
+ height: auto !important;
+ visibility: visible !important;
+ background-color: transparent !important;
+ border: 0 !important;
+ transform: none !important;
+ transition: none;
+ }
+ .navbar-expand-md .offcanvas .offcanvas-header {
+ display: none;
+ }
+ .navbar-expand-md .offcanvas .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ }
+}
+@media (min-width: 992px) {
+ .navbar-expand-lg {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+ .navbar-expand-lg .navbar-nav {
+ flex-direction: row;
+ }
+ .navbar-expand-lg .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ .navbar-expand-lg .navbar-nav .nav-link {
+ padding-right: var(--bs-navbar-nav-link-padding-x);
+ padding-left: var(--bs-navbar-nav-link-padding-x);
+ }
+ .navbar-expand-lg .navbar-nav-scroll {
+ overflow: visible;
+ }
+ .navbar-expand-lg .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+ .navbar-expand-lg .navbar-toggler {
+ display: none;
+ }
+ .navbar-expand-lg .offcanvas {
+ position: static;
+ z-index: auto;
+ flex-grow: 1;
+ width: auto !important;
+ height: auto !important;
+ visibility: visible !important;
+ background-color: transparent !important;
+ border: 0 !important;
+ transform: none !important;
+ transition: none;
+ }
+ .navbar-expand-lg .offcanvas .offcanvas-header {
+ display: none;
+ }
+ .navbar-expand-lg .offcanvas .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ }
+}
+@media (min-width: 1200px) {
+ .navbar-expand-xl {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+ .navbar-expand-xl .navbar-nav {
+ flex-direction: row;
+ }
+ .navbar-expand-xl .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ .navbar-expand-xl .navbar-nav .nav-link {
+ padding-right: var(--bs-navbar-nav-link-padding-x);
+ padding-left: var(--bs-navbar-nav-link-padding-x);
+ }
+ .navbar-expand-xl .navbar-nav-scroll {
+ overflow: visible;
+ }
+ .navbar-expand-xl .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+ .navbar-expand-xl .navbar-toggler {
+ display: none;
+ }
+ .navbar-expand-xl .offcanvas {
+ position: static;
+ z-index: auto;
+ flex-grow: 1;
+ width: auto !important;
+ height: auto !important;
+ visibility: visible !important;
+ background-color: transparent !important;
+ border: 0 !important;
+ transform: none !important;
+ transition: none;
+ }
+ .navbar-expand-xl .offcanvas .offcanvas-header {
+ display: none;
+ }
+ .navbar-expand-xl .offcanvas .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ }
+}
+@media (min-width: 1400px) {
+ .navbar-expand-xxl {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+ .navbar-expand-xxl .navbar-nav {
+ flex-direction: row;
+ }
+ .navbar-expand-xxl .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ .navbar-expand-xxl .navbar-nav .nav-link {
+ padding-right: var(--bs-navbar-nav-link-padding-x);
+ padding-left: var(--bs-navbar-nav-link-padding-x);
+ }
+ .navbar-expand-xxl .navbar-nav-scroll {
+ overflow: visible;
+ }
+ .navbar-expand-xxl .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+ .navbar-expand-xxl .navbar-toggler {
+ display: none;
+ }
+ .navbar-expand-xxl .offcanvas {
+ position: static;
+ z-index: auto;
+ flex-grow: 1;
+ width: auto !important;
+ height: auto !important;
+ visibility: visible !important;
+ background-color: transparent !important;
+ border: 0 !important;
+ transform: none !important;
+ transition: none;
+ }
+ .navbar-expand-xxl .offcanvas .offcanvas-header {
+ display: none;
+ }
+ .navbar-expand-xxl .offcanvas .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ }
+}
+.navbar-expand {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+}
+.navbar-expand .navbar-nav {
+ flex-direction: row;
+}
+.navbar-expand .navbar-nav .dropdown-menu {
+ position: absolute;
+}
+.navbar-expand .navbar-nav .nav-link {
+ padding-right: var(--bs-navbar-nav-link-padding-x);
+ padding-left: var(--bs-navbar-nav-link-padding-x);
+}
+.navbar-expand .navbar-nav-scroll {
+ overflow: visible;
+}
+.navbar-expand .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+}
+.navbar-expand .navbar-toggler {
+ display: none;
+}
+.navbar-expand .offcanvas {
+ position: static;
+ z-index: auto;
+ flex-grow: 1;
+ width: auto !important;
+ height: auto !important;
+ visibility: visible !important;
+ background-color: transparent !important;
+ border: 0 !important;
+ transform: none !important;
+ transition: none;
+}
+.navbar-expand .offcanvas .offcanvas-header {
+ display: none;
+}
+.navbar-expand .offcanvas .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+}
+.navbar-dark,
+.navbar[data-bs-theme="dark"] {
+ --bs-navbar-color: rgba(255, 255, 255, 0.55);
+ --bs-navbar-hover-color: rgba(255, 255, 255, 0.75);
+ --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);
+ --bs-navbar-active-color: #fff;
+ --bs-navbar-brand-color: #fff;
+ --bs-navbar-brand-hover-color: #fff;
+ --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);
+ --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+}
+[data-bs-theme="dark"] .navbar-toggler-icon {
+ --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+}
+.card {
+ --bs-card-spacer-y: 1rem;
+ --bs-card-spacer-x: 1rem;
+ --bs-card-title-spacer-y: 0.5rem;
+ --bs-card-title-color: ;
+ --bs-card-subtitle-color: ;
+ --bs-card-border-width: var(--bs-border-width);
+ --bs-card-border-color: var(--bs-border-color-translucent);
+ --bs-card-border-radius: var(--bs-border-radius);
+ --bs-card-box-shadow: ;
+ --bs-card-inner-border-radius: calc(
+ var(--bs-border-radius) - (var(--bs-border-width))
+ );
+ --bs-card-cap-padding-y: 0.5rem;
+ --bs-card-cap-padding-x: 1rem;
+ --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);
+ --bs-card-cap-color: ;
+ --bs-card-height: ;
+ --bs-card-color: ;
+ --bs-card-bg: var(--bs-body-bg);
+ --bs-card-img-overlay-padding: 1rem;
+ --bs-card-group-margin: 0.75rem;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ height: var(--bs-card-height);
+ color: var(--bs-body-color);
+ word-wrap: break-word;
+ background-color: var(--bs-card-bg);
+ background-clip: border-box;
+ border: var(--bs-card-border-width) solid
+ var(--bs-card-border-color);
+ border-radius: var(--bs-card-border-radius);
+}
+.card > hr {
+ margin-right: 0;
+ margin-left: 0;
+}
+.card > .list-group {
+ border-top: inherit;
+ border-bottom: inherit;
+}
+.card > .list-group:first-child {
+ border-top-width: 0;
+ border-top-left-radius: var(--bs-card-inner-border-radius);
+ border-top-right-radius: var(--bs-card-inner-border-radius);
+}
+.card > .list-group:last-child {
+ border-bottom-width: 0;
+ border-bottom-right-radius: var(--bs-card-inner-border-radius);
+ border-bottom-left-radius: var(--bs-card-inner-border-radius);
+}
+.card > .card-header + .list-group,
+.card > .list-group + .card-footer {
+ border-top: 0;
+}
+.card-body {
+ flex: 1 1 auto;
+ padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);
+ color: var(--bs-card-color);
+}
+.card-title {
+ margin-bottom: var(--bs-card-title-spacer-y);
+ color: var(--bs-card-title-color);
+}
+.card-subtitle {
+ margin-top: calc(-0.5 * var(--bs-card-title-spacer-y));
+ margin-bottom: 0;
+ color: var(--bs-card-subtitle-color);
+}
+.card-text:last-child {
+ margin-bottom: 0;
+}
+.card-link + .card-link {
+ margin-left: var(--bs-card-spacer-x);
+}
+.card-header {
+ padding: var(--bs-card-cap-padding-y)
+ var(--bs-card-cap-padding-x);
+ margin-bottom: 0;
+ color: var(--bs-card-cap-color);
+ background-color: var(--bs-card-cap-bg);
+ border-bottom: var(--bs-card-border-width) solid
+ var(--bs-card-border-color);
+}
+.card-header:first-child {
+ border-radius: var(--bs-card-inner-border-radius)
+ var(--bs-card-inner-border-radius) 0 0;
+}
+.card-footer {
+ padding: var(--bs-card-cap-padding-y)
+ var(--bs-card-cap-padding-x);
+ color: var(--bs-card-cap-color);
+ background-color: var(--bs-card-cap-bg);
+ border-top: var(--bs-card-border-width) solid
+ var(--bs-card-border-color);
+}
+.card-footer:last-child {
+ border-radius: 0 0 var(--bs-card-inner-border-radius)
+ var(--bs-card-inner-border-radius);
+}
+.card-header-tabs {
+ margin-right: calc(-0.5 * var(--bs-card-cap-padding-x));
+ margin-bottom: calc(-1 * var(--bs-card-cap-padding-y));
+ margin-left: calc(-0.5 * var(--bs-card-cap-padding-x));
+ border-bottom: 0;
+}
+.card-header-tabs .nav-link.active {
+ background-color: var(--bs-card-bg);
+ border-bottom-color: var(--bs-card-bg);
+}
+.card-header-pills {
+ margin-right: calc(-0.5 * var(--bs-card-cap-padding-x));
+ margin-left: calc(-0.5 * var(--bs-card-cap-padding-x));
+}
+.card-img-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ padding: var(--bs-card-img-overlay-padding);
+ border-radius: var(--bs-card-inner-border-radius);
+}
+.card-img,
+.card-img-bottom,
+.card-img-top {
+ width: 100%;
+}
+.card-img,
+.card-img-top {
+ border-top-left-radius: var(--bs-card-inner-border-radius);
+ border-top-right-radius: var(--bs-card-inner-border-radius);
+}
+.card-img,
+.card-img-bottom {
+ border-bottom-right-radius: var(--bs-card-inner-border-radius);
+ border-bottom-left-radius: var(--bs-card-inner-border-radius);
+}
+.card-group > .card {
+ margin-bottom: var(--bs-card-group-margin);
+}
+@media (min-width: 576px) {
+ .card-group {
+ display: flex;
+ flex-flow: row wrap;
+ }
+ .card-group > .card {
+ flex: 1 0 0%;
+ margin-bottom: 0;
+ }
+ .card-group > .card + .card {
+ margin-left: 0;
+ border-left: 0;
+ }
+ .card-group > .card:not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ .card-group > .card:not(:last-child) .card-header,
+ .card-group > .card:not(:last-child) .card-img-top {
+ border-top-right-radius: 0;
+ }
+ .card-group > .card:not(:last-child) .card-footer,
+ .card-group > .card:not(:last-child) .card-img-bottom {
+ border-bottom-right-radius: 0;
+ }
+ .card-group > .card:not(:first-child) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ .card-group > .card:not(:first-child) .card-header,
+ .card-group > .card:not(:first-child) .card-img-top {
+ border-top-left-radius: 0;
+ }
+ .card-group > .card:not(:first-child) .card-footer,
+ .card-group > .card:not(:first-child) .card-img-bottom {
+ border-bottom-left-radius: 0;
+ }
+}
+.accordion {
+ --bs-accordion-color: var(--bs-body-color);
+ --bs-accordion-bg: var(--bs-body-bg);
+ --bs-accordion-transition: color 0.15s ease-in-out,
+ background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out,
+ border-radius 0.15s ease;
+ --bs-accordion-border-color: var(--bs-border-color);
+ --bs-accordion-border-width: var(--bs-border-width);
+ --bs-accordion-border-radius: var(--bs-border-radius);
+ --bs-accordion-inner-border-radius: calc(
+ var(--bs-border-radius) - (var(--bs-border-width))
+ );
+ --bs-accordion-btn-padding-x: 1.25rem;
+ --bs-accordion-btn-padding-y: 1rem;
+ --bs-accordion-btn-color: var(--bs-body-color);
+ --bs-accordion-btn-bg: var(--bs-accordion-bg);
+ --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+ --bs-accordion-btn-icon-width: 1.25rem;
+ --bs-accordion-btn-icon-transform: rotate(-180deg);
+ --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;
+ --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23052c65'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+ --bs-accordion-btn-focus-border-color: #86b7fe;
+ --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem
+ rgba(13, 110, 253, 0.25);
+ --bs-accordion-body-padding-x: 1.25rem;
+ --bs-accordion-body-padding-y: 1rem;
+ --bs-accordion-active-color: var(--bs-primary-text-emphasis);
+ --bs-accordion-active-bg: var(--bs-primary-bg-subtle);
+}
+.accordion-button {
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ padding: var(--bs-accordion-btn-padding-y)
+ var(--bs-accordion-btn-padding-x);
+ font-size: 1rem;
+ color: var(--bs-accordion-btn-color);
+ text-align: left;
+ background-color: var(--bs-accordion-btn-bg);
+ border: 0;
+ border-radius: 0;
+ overflow-anchor: none;
+ transition: var(--bs-accordion-transition);
+}
+@media (prefers-reduced-motion: reduce) {
+ .accordion-button {
+ transition: none;
+ }
+}
+.accordion-button:not(.collapsed) {
+ color: var(--bs-accordion-active-color);
+ background-color: var(--bs-accordion-active-bg);
+ box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width))
+ 0 var(--bs-accordion-border-color);
+}
+.accordion-button:not(.collapsed)::after {
+ background-image: var(--bs-accordion-btn-active-icon);
+ transform: var(--bs-accordion-btn-icon-transform);
+}
+.accordion-button::after {
+ flex-shrink: 0;
+ width: var(--bs-accordion-btn-icon-width);
+ height: var(--bs-accordion-btn-icon-width);
+ margin-left: auto;
+ content: "";
+ background-image: var(--bs-accordion-btn-icon);
+ background-repeat: no-repeat;
+ background-size: var(--bs-accordion-btn-icon-width);
+ transition: var(--bs-accordion-btn-icon-transition);
+}
+@media (prefers-reduced-motion: reduce) {
+ .accordion-button::after {
+ transition: none;
+ }
+}
+.accordion-button:hover {
+ z-index: 2;
+}
+.accordion-button:focus {
+ z-index: 3;
+ border-color: var(--bs-accordion-btn-focus-border-color);
+ outline: 0;
+ box-shadow: var(--bs-accordion-btn-focus-box-shadow);
+}
+.accordion-header {
+ margin-bottom: 0;
+}
+.accordion-item {
+ color: var(--bs-accordion-color);
+ background-color: var(--bs-accordion-bg);
+ border: var(--bs-accordion-border-width) solid
+ var(--bs-accordion-border-color);
+}
+.accordion-item:first-of-type {
+ border-top-left-radius: var(--bs-accordion-border-radius);
+ border-top-right-radius: var(--bs-accordion-border-radius);
+}
+.accordion-item:first-of-type .accordion-button {
+ border-top-left-radius: var(--bs-accordion-inner-border-radius);
+ border-top-right-radius: var(
+ --bs-accordion-inner-border-radius
+ );
+}
+.accordion-item:not(:first-of-type) {
+ border-top: 0;
+}
+.accordion-item:last-of-type {
+ border-bottom-right-radius: var(--bs-accordion-border-radius);
+ border-bottom-left-radius: var(--bs-accordion-border-radius);
+}
+.accordion-item:last-of-type .accordion-button.collapsed {
+ border-bottom-right-radius: var(
+ --bs-accordion-inner-border-radius
+ );
+ border-bottom-left-radius: var(
+ --bs-accordion-inner-border-radius
+ );
+}
+.accordion-item:last-of-type .accordion-collapse {
+ border-bottom-right-radius: var(--bs-accordion-border-radius);
+ border-bottom-left-radius: var(--bs-accordion-border-radius);
+}
+.accordion-body {
+ padding: var(--bs-accordion-body-padding-y)
+ var(--bs-accordion-body-padding-x);
+}
+.accordion-flush .accordion-collapse {
+ border-width: 0;
+}
+.accordion-flush .accordion-item {
+ border-right: 0;
+ border-left: 0;
+ border-radius: 0;
+}
+.accordion-flush .accordion-item:first-child {
+ border-top: 0;
+}
+.accordion-flush .accordion-item:last-child {
+ border-bottom: 0;
+}
+.accordion-flush .accordion-item .accordion-button,
+.accordion-flush .accordion-item .accordion-button.collapsed {
+ border-radius: 0;
+}
+[data-bs-theme="dark"] .accordion-button::after {
+ --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+ --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+}
+.breadcrumb {
+ --bs-breadcrumb-padding-x: 0;
+ --bs-breadcrumb-padding-y: 0;
+ --bs-breadcrumb-margin-bottom: 1rem;
+ --bs-breadcrumb-bg: ;
+ --bs-breadcrumb-border-radius: ;
+ --bs-breadcrumb-divider-color: var(--bs-secondary-color);
+ --bs-breadcrumb-item-padding-x: 0.5rem;
+ --bs-breadcrumb-item-active-color: var(--bs-secondary-color);
+ display: flex;
+ flex-wrap: wrap;
+ padding: var(--bs-breadcrumb-padding-y)
+ var(--bs-breadcrumb-padding-x);
+ margin-bottom: var(--bs-breadcrumb-margin-bottom);
+ font-size: var(--bs-breadcrumb-font-size);
+ list-style: none;
+ background-color: var(--bs-breadcrumb-bg);
+ border-radius: var(--bs-breadcrumb-border-radius);
+}
+.breadcrumb-item + .breadcrumb-item {
+ padding-left: var(--bs-breadcrumb-item-padding-x);
+}
+.breadcrumb-item + .breadcrumb-item::before {
+ float: left;
+ padding-right: var(--bs-breadcrumb-item-padding-x);
+ color: var(--bs-breadcrumb-divider-color);
+ content: var(--bs-breadcrumb-divider, "/");
+}
+.breadcrumb-item.active {
+ color: var(--bs-breadcrumb-item-active-color);
+}
+.pagination {
+ --bs-pagination-padding-x: 0.75rem;
+ --bs-pagination-padding-y: 0.375rem;
+ --bs-pagination-font-size: 1rem;
+ --bs-pagination-color: var(--bs-link-color);
+ --bs-pagination-bg: var(--bs-body-bg);
+ --bs-pagination-border-width: var(--bs-border-width);
+ --bs-pagination-border-color: var(--bs-border-color);
+ --bs-pagination-border-radius: var(--bs-border-radius);
+ --bs-pagination-hover-color: var(--bs-link-hover-color);
+ --bs-pagination-hover-bg: var(--bs-tertiary-bg);
+ --bs-pagination-hover-border-color: var(--bs-border-color);
+ --bs-pagination-focus-color: var(--bs-link-hover-color);
+ --bs-pagination-focus-bg: var(--bs-secondary-bg);
+ --bs-pagination-focus-box-shadow: 0 0 0 0.25rem
+ rgba(13, 110, 253, 0.25);
+ --bs-pagination-active-color: #fff;
+ --bs-pagination-active-bg: #0d6efd;
+ --bs-pagination-active-border-color: #0d6efd;
+ --bs-pagination-disabled-color: var(--bs-secondary-color);
+ --bs-pagination-disabled-bg: var(--bs-secondary-bg);
+ --bs-pagination-disabled-border-color: var(--bs-border-color);
+ display: flex;
+ padding-left: 0;
+ list-style: none;
+}
+.page-link {
+ position: relative;
+ display: block;
+ padding: var(--bs-pagination-padding-y)
+ var(--bs-pagination-padding-x);
+ font-size: var(--bs-pagination-font-size);
+ color: var(--bs-pagination-color);
+ text-decoration: none;
+ background-color: var(--bs-pagination-bg);
+ border: var(--bs-pagination-border-width) solid
+ var(--bs-pagination-border-color);
+ transition: color 0.15s ease-in-out,
+ background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .page-link {
+ transition: none;
+ }
+}
+.page-link:hover {
+ z-index: 2;
+ color: var(--bs-pagination-hover-color);
+ background-color: var(--bs-pagination-hover-bg);
+ border-color: var(--bs-pagination-hover-border-color);
+}
+.page-link:focus {
+ z-index: 3;
+ color: var(--bs-pagination-focus-color);
+ background-color: var(--bs-pagination-focus-bg);
+ outline: 0;
+ box-shadow: var(--bs-pagination-focus-box-shadow);
+}
+.active > .page-link,
+.page-link.active {
+ z-index: 3;
+ color: var(--bs-pagination-active-color);
+ background-color: var(--bs-pagination-active-bg);
+ border-color: var(--bs-pagination-active-border-color);
+}
+.disabled > .page-link,
+.page-link.disabled {
+ color: var(--bs-pagination-disabled-color);
+ pointer-events: none;
+ background-color: var(--bs-pagination-disabled-bg);
+ border-color: var(--bs-pagination-disabled-border-color);
+}
+.page-item:not(:first-child) .page-link {
+ margin-left: calc(var(--bs-border-width) * -1);
+}
+.page-item:first-child .page-link {
+ border-top-left-radius: var(--bs-pagination-border-radius);
+ border-bottom-left-radius: var(--bs-pagination-border-radius);
+}
+.page-item:last-child .page-link {
+ border-top-right-radius: var(--bs-pagination-border-radius);
+ border-bottom-right-radius: var(--bs-pagination-border-radius);
+}
+.pagination-lg {
+ --bs-pagination-padding-x: 1.5rem;
+ --bs-pagination-padding-y: 0.75rem;
+ --bs-pagination-font-size: 1.25rem;
+ --bs-pagination-border-radius: var(--bs-border-radius-lg);
+}
+.pagination-sm {
+ --bs-pagination-padding-x: 0.5rem;
+ --bs-pagination-padding-y: 0.25rem;
+ --bs-pagination-font-size: 0.875rem;
+ --bs-pagination-border-radius: var(--bs-border-radius-sm);
+}
+.badge {
+ --bs-badge-padding-x: 0.65em;
+ --bs-badge-padding-y: 0.35em;
+ --bs-badge-font-size: 0.75em;
+ --bs-badge-font-weight: 700;
+ --bs-badge-color: #fff;
+ --bs-badge-border-radius: var(--bs-border-radius);
+ display: inline-block;
+ padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x);
+ font-size: var(--bs-badge-font-size);
+ font-weight: var(--bs-badge-font-weight);
+ line-height: 1;
+ color: var(--bs-badge-color);
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: var(--bs-badge-border-radius);
+}
+.badge:empty {
+ display: none;
+}
+.btn .badge {
+ position: relative;
+ top: -1px;
+}
+.alert {
+ --bs-alert-bg: transparent;
+ --bs-alert-padding-x: 1rem;
+ --bs-alert-padding-y: 1rem;
+ --bs-alert-margin-bottom: 1rem;
+ --bs-alert-color: inherit;
+ --bs-alert-border-color: transparent;
+ --bs-alert-border: var(--bs-border-width) solid
+ var(--bs-alert-border-color);
+ --bs-alert-border-radius: var(--bs-border-radius);
+ --bs-alert-link-color: inherit;
+ position: relative;
+ padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x);
+ margin-bottom: var(--bs-alert-margin-bottom);
+ color: var(--bs-alert-color);
+ background-color: var(--bs-alert-bg);
+ border: var(--bs-alert-border);
+ border-radius: var(--bs-alert-border-radius);
+}
+.alert-heading {
+ color: inherit;
+}
+.alert-link {
+ font-weight: 700;
+ color: var(--bs-alert-link-color);
+}
+.alert-dismissible {
+ padding-right: 3rem;
+}
+.alert-dismissible .btn-close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 2;
+ padding: 1.25rem 1rem;
+}
+.alert-primary {
+ --bs-alert-color: var(--bs-primary-text-emphasis);
+ --bs-alert-bg: var(--bs-primary-bg-subtle);
+ --bs-alert-border-color: var(--bs-primary-border-subtle);
+ --bs-alert-link-color: var(--bs-primary-text-emphasis);
+}
+.alert-secondary {
+ --bs-alert-color: var(--bs-secondary-text-emphasis);
+ --bs-alert-bg: var(--bs-secondary-bg-subtle);
+ --bs-alert-border-color: var(--bs-secondary-border-subtle);
+ --bs-alert-link-color: var(--bs-secondary-text-emphasis);
+}
+.alert-success {
+ --bs-alert-color: var(--bs-success-text-emphasis);
+ --bs-alert-bg: var(--bs-success-bg-subtle);
+ --bs-alert-border-color: var(--bs-success-border-subtle);
+ --bs-alert-link-color: var(--bs-success-text-emphasis);
+}
+.alert-info {
+ --bs-alert-color: var(--bs-info-text-emphasis);
+ --bs-alert-bg: var(--bs-info-bg-subtle);
+ --bs-alert-border-color: var(--bs-info-border-subtle);
+ --bs-alert-link-color: var(--bs-info-text-emphasis);
+}
+.alert-warning {
+ --bs-alert-color: var(--bs-warning-text-emphasis);
+ --bs-alert-bg: var(--bs-warning-bg-subtle);
+ --bs-alert-border-color: var(--bs-warning-border-subtle);
+ --bs-alert-link-color: var(--bs-warning-text-emphasis);
+}
+.alert-danger {
+ --bs-alert-color: var(--bs-danger-text-emphasis);
+ --bs-alert-bg: var(--bs-danger-bg-subtle);
+ --bs-alert-border-color: var(--bs-danger-border-subtle);
+ --bs-alert-link-color: var(--bs-danger-text-emphasis);
+}
+.alert-light {
+ --bs-alert-color: var(--bs-light-text-emphasis);
+ --bs-alert-bg: var(--bs-light-bg-subtle);
+ --bs-alert-border-color: var(--bs-light-border-subtle);
+ --bs-alert-link-color: var(--bs-light-text-emphasis);
+}
+.alert-dark {
+ --bs-alert-color: var(--bs-dark-text-emphasis);
+ --bs-alert-bg: var(--bs-dark-bg-subtle);
+ --bs-alert-border-color: var(--bs-dark-border-subtle);
+ --bs-alert-link-color: var(--bs-dark-text-emphasis);
+}
+@keyframes progress-bar-stripes {
+ 0% {
+ background-position-x: 1rem;
+ }
+}
+.progress,
+.progress-stacked {
+ --bs-progress-height: 1rem;
+ --bs-progress-font-size: 0.75rem;
+ --bs-progress-bg: var(--bs-secondary-bg);
+ --bs-progress-border-radius: var(--bs-border-radius);
+ --bs-progress-box-shadow: var(--bs-box-shadow-inset);
+ --bs-progress-bar-color: #fff;
+ --bs-progress-bar-bg: #0d6efd;
+ --bs-progress-bar-transition: width 0.6s ease;
+ display: flex;
+ height: var(--bs-progress-height);
+ overflow: hidden;
+ font-size: var(--bs-progress-font-size);
+ background-color: var(--bs-progress-bg);
+ border-radius: var(--bs-progress-border-radius);
+}
+.progress-bar {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ overflow: hidden;
+ color: var(--bs-progress-bar-color);
+ text-align: center;
+ white-space: nowrap;
+ background-color: var(--bs-progress-bar-bg);
+ transition: var(--bs-progress-bar-transition);
+}
+@media (prefers-reduced-motion: reduce) {
+ .progress-bar {
+ transition: none;
+ }
+}
+.progress-bar-striped {
+ background-image: linear-gradient(
+ 45deg,
+ rgba(255, 255, 255, 0.15) 25%,
+ transparent 25%,
+ transparent 50%,
+ rgba(255, 255, 255, 0.15) 50%,
+ rgba(255, 255, 255, 0.15) 75%,
+ transparent 75%,
+ transparent
+ );
+ background-size: var(--bs-progress-height)
+ var(--bs-progress-height);
+}
+.progress-stacked > .progress {
+ overflow: visible;
+}
+.progress-stacked > .progress > .progress-bar {
+ width: 100%;
+}
+.progress-bar-animated {
+ animation: 1s linear infinite progress-bar-stripes;
+}
+@media (prefers-reduced-motion: reduce) {
+ .progress-bar-animated {
+ animation: none;
+ }
+}
+.list-group {
+ --bs-list-group-color: var(--bs-body-color);
+ --bs-list-group-bg: var(--bs-body-bg);
+ --bs-list-group-border-color: var(--bs-border-color);
+ --bs-list-group-border-width: var(--bs-border-width);
+ --bs-list-group-border-radius: var(--bs-border-radius);
+ --bs-list-group-item-padding-x: 1rem;
+ --bs-list-group-item-padding-y: 0.5rem;
+ --bs-list-group-action-color: var(--bs-secondary-color);
+ --bs-list-group-action-hover-color: var(--bs-emphasis-color);
+ --bs-list-group-action-hover-bg: var(--bs-tertiary-bg);
+ --bs-list-group-action-active-color: var(--bs-body-color);
+ --bs-list-group-action-active-bg: var(--bs-secondary-bg);
+ --bs-list-group-disabled-color: var(--bs-secondary-color);
+ --bs-list-group-disabled-bg: var(--bs-body-bg);
+ --bs-list-group-active-color: #fff;
+ --bs-list-group-active-bg: #0d6efd;
+ --bs-list-group-active-border-color: #0d6efd;
+ display: flex;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ border-radius: var(--bs-list-group-border-radius);
+}
+.list-group-numbered {
+ list-style-type: none;
+ counter-reset: section;
+}
+.list-group-numbered > .list-group-item::before {
+ content: counters(section, ".") ". ";
+ counter-increment: section;
+}
+.list-group-item-action {
+ width: 100%;
+ color: var(--bs-list-group-action-color);
+ text-align: inherit;
+}
+.list-group-item-action:focus,
+.list-group-item-action:hover {
+ z-index: 1;
+ color: var(--bs-list-group-action-hover-color);
+ text-decoration: none;
+ background-color: var(--bs-list-group-action-hover-bg);
+}
+.list-group-item-action:active {
+ color: var(--bs-list-group-action-active-color);
+ background-color: var(--bs-list-group-action-active-bg);
+}
+.list-group-item {
+ position: relative;
+ display: block;
+ padding: var(--bs-list-group-item-padding-y)
+ var(--bs-list-group-item-padding-x);
+ color: var(--bs-list-group-color);
+ text-decoration: none;
+ background-color: var(--bs-list-group-bg);
+ border: var(--bs-list-group-border-width) solid
+ var(--bs-list-group-border-color);
+}
+.list-group-item:first-child {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
+.list-group-item:last-child {
+ border-bottom-right-radius: inherit;
+ border-bottom-left-radius: inherit;
+}
+.list-group-item.disabled,
+.list-group-item:disabled {
+ color: var(--bs-list-group-disabled-color);
+ pointer-events: none;
+ background-color: var(--bs-list-group-disabled-bg);
+}
+.list-group-item.active {
+ z-index: 2;
+ color: var(--bs-list-group-active-color);
+ background-color: var(--bs-list-group-active-bg);
+ border-color: var(--bs-list-group-active-border-color);
+}
+.list-group-item + .list-group-item {
+ border-top-width: 0;
+}
+.list-group-item + .list-group-item.active {
+ margin-top: calc(-1 * var(--bs-list-group-border-width));
+ border-top-width: var(--bs-list-group-border-width);
+}
+.list-group-horizontal {
+ flex-direction: row;
+}
+.list-group-horizontal
+ > .list-group-item:first-child:not(:last-child) {
+ border-bottom-left-radius: var(--bs-list-group-border-radius);
+ border-top-right-radius: 0;
+}
+.list-group-horizontal
+ > .list-group-item:last-child:not(:first-child) {
+ border-top-right-radius: var(--bs-list-group-border-radius);
+ border-bottom-left-radius: 0;
+}
+.list-group-horizontal > .list-group-item.active {
+ margin-top: 0;
+}
+.list-group-horizontal > .list-group-item + .list-group-item {
+ border-top-width: var(--bs-list-group-border-width);
+ border-left-width: 0;
+}
+.list-group-horizontal
+ > .list-group-item
+ + .list-group-item.active {
+ margin-left: calc(-1 * var(--bs-list-group-border-width));
+ border-left-width: var(--bs-list-group-border-width);
+}
+@media (min-width: 576px) {
+ .list-group-horizontal-sm {
+ flex-direction: row;
+ }
+ .list-group-horizontal-sm
+ > .list-group-item:first-child:not(:last-child) {
+ border-bottom-left-radius: var(
+ --bs-list-group-border-radius
+ );
+ border-top-right-radius: 0;
+ }
+ .list-group-horizontal-sm
+ > .list-group-item:last-child:not(:first-child) {
+ border-top-right-radius: var(--bs-list-group-border-radius);
+ border-bottom-left-radius: 0;
+ }
+ .list-group-horizontal-sm > .list-group-item.active {
+ margin-top: 0;
+ }
+ .list-group-horizontal-sm
+ > .list-group-item
+ + .list-group-item {
+ border-top-width: var(--bs-list-group-border-width);
+ border-left-width: 0;
+ }
+ .list-group-horizontal-sm
+ > .list-group-item
+ + .list-group-item.active {
+ margin-left: calc(-1 * var(--bs-list-group-border-width));
+ border-left-width: var(--bs-list-group-border-width);
+ }
+}
+@media (min-width: 768px) {
+ .list-group-horizontal-md {
+ flex-direction: row;
+ }
+ .list-group-horizontal-md
+ > .list-group-item:first-child:not(:last-child) {
+ border-bottom-left-radius: var(
+ --bs-list-group-border-radius
+ );
+ border-top-right-radius: 0;
+ }
+ .list-group-horizontal-md
+ > .list-group-item:last-child:not(:first-child) {
+ border-top-right-radius: var(--bs-list-group-border-radius);
+ border-bottom-left-radius: 0;
+ }
+ .list-group-horizontal-md > .list-group-item.active {
+ margin-top: 0;
+ }
+ .list-group-horizontal-md
+ > .list-group-item
+ + .list-group-item {
+ border-top-width: var(--bs-list-group-border-width);
+ border-left-width: 0;
+ }
+ .list-group-horizontal-md
+ > .list-group-item
+ + .list-group-item.active {
+ margin-left: calc(-1 * var(--bs-list-group-border-width));
+ border-left-width: var(--bs-list-group-border-width);
+ }
+}
+@media (min-width: 992px) {
+ .list-group-horizontal-lg {
+ flex-direction: row;
+ }
+ .list-group-horizontal-lg
+ > .list-group-item:first-child:not(:last-child) {
+ border-bottom-left-radius: var(
+ --bs-list-group-border-radius
+ );
+ border-top-right-radius: 0;
+ }
+ .list-group-horizontal-lg
+ > .list-group-item:last-child:not(:first-child) {
+ border-top-right-radius: var(--bs-list-group-border-radius);
+ border-bottom-left-radius: 0;
+ }
+ .list-group-horizontal-lg > .list-group-item.active {
+ margin-top: 0;
+ }
+ .list-group-horizontal-lg
+ > .list-group-item
+ + .list-group-item {
+ border-top-width: var(--bs-list-group-border-width);
+ border-left-width: 0;
+ }
+ .list-group-horizontal-lg
+ > .list-group-item
+ + .list-group-item.active {
+ margin-left: calc(-1 * var(--bs-list-group-border-width));
+ border-left-width: var(--bs-list-group-border-width);
+ }
+}
+@media (min-width: 1200px) {
+ .list-group-horizontal-xl {
+ flex-direction: row;
+ }
+ .list-group-horizontal-xl
+ > .list-group-item:first-child:not(:last-child) {
+ border-bottom-left-radius: var(
+ --bs-list-group-border-radius
+ );
+ border-top-right-radius: 0;
+ }
+ .list-group-horizontal-xl
+ > .list-group-item:last-child:not(:first-child) {
+ border-top-right-radius: var(--bs-list-group-border-radius);
+ border-bottom-left-radius: 0;
+ }
+ .list-group-horizontal-xl > .list-group-item.active {
+ margin-top: 0;
+ }
+ .list-group-horizontal-xl
+ > .list-group-item
+ + .list-group-item {
+ border-top-width: var(--bs-list-group-border-width);
+ border-left-width: 0;
+ }
+ .list-group-horizontal-xl
+ > .list-group-item
+ + .list-group-item.active {
+ margin-left: calc(-1 * var(--bs-list-group-border-width));
+ border-left-width: var(--bs-list-group-border-width);
+ }
+}
+@media (min-width: 1400px) {
+ .list-group-horizontal-xxl {
+ flex-direction: row;
+ }
+ .list-group-horizontal-xxl
+ > .list-group-item:first-child:not(:last-child) {
+ border-bottom-left-radius: var(
+ --bs-list-group-border-radius
+ );
+ border-top-right-radius: 0;
+ }
+ .list-group-horizontal-xxl
+ > .list-group-item:last-child:not(:first-child) {
+ border-top-right-radius: var(--bs-list-group-border-radius);
+ border-bottom-left-radius: 0;
+ }
+ .list-group-horizontal-xxl > .list-group-item.active {
+ margin-top: 0;
+ }
+ .list-group-horizontal-xxl
+ > .list-group-item
+ + .list-group-item {
+ border-top-width: var(--bs-list-group-border-width);
+ border-left-width: 0;
+ }
+ .list-group-horizontal-xxl
+ > .list-group-item
+ + .list-group-item.active {
+ margin-left: calc(-1 * var(--bs-list-group-border-width));
+ border-left-width: var(--bs-list-group-border-width);
+ }
+}
+.list-group-flush {
+ border-radius: 0;
+}
+.list-group-flush > .list-group-item {
+ border-width: 0 0 var(--bs-list-group-border-width);
+}
+.list-group-flush > .list-group-item:last-child {
+ border-bottom-width: 0;
+}
+.list-group-item-primary {
+ --bs-list-group-color: var(--bs-primary-text-emphasis);
+ --bs-list-group-bg: var(--bs-primary-bg-subtle);
+ --bs-list-group-border-color: var(--bs-primary-border-subtle);
+ --bs-list-group-action-hover-color: var(--bs-emphasis-color);
+ --bs-list-group-action-hover-bg: var(
+ --bs-primary-border-subtle
+ );
+ --bs-list-group-action-active-color: var(--bs-emphasis-color);
+ --bs-list-group-action-active-bg: var(
+ --bs-primary-border-subtle
+ );
+ --bs-list-group-active-color: var(--bs-primary-bg-subtle);
+ --bs-list-group-active-bg: var(--bs-primary-text-emphasis);
+ --bs-list-group-active-border-color: var(
+ --bs-primary-text-emphasis
+ );
+}
+.list-group-item-secondary {
+ --bs-list-group-color: var(--bs-secondary-text-emphasis);
+ --bs-list-group-bg: var(--bs-secondary-bg-subtle);
+ --bs-list-group-border-color: var(--bs-secondary-border-subtle);
+ --bs-list-group-action-hover-color: var(--bs-emphasis-color);
+ --bs-list-group-action-hover-bg: var(
+ --bs-secondary-border-subtle
+ );
+ --bs-list-group-action-active-color: var(--bs-emphasis-color);
+ --bs-list-group-action-active-bg: var(
+ --bs-secondary-border-subtle
+ );
+ --bs-list-group-active-color: var(--bs-secondary-bg-subtle);
+ --bs-list-group-active-bg: var(--bs-secondary-text-emphasis);
+ --bs-list-group-active-border-color: var(
+ --bs-secondary-text-emphasis
+ );
+}
+.list-group-item-success {
+ --bs-list-group-color: var(--bs-success-text-emphasis);
+ --bs-list-group-bg: var(--bs-success-bg-subtle);
+ --bs-list-group-border-color: var(--bs-success-border-subtle);
+ --bs-list-group-action-hover-color: var(--bs-emphasis-color);
+ --bs-list-group-action-hover-bg: var(
+ --bs-success-border-subtle
+ );
+ --bs-list-group-action-active-color: var(--bs-emphasis-color);
+ --bs-list-group-action-active-bg: var(
+ --bs-success-border-subtle
+ );
+ --bs-list-group-active-color: var(--bs-success-bg-subtle);
+ --bs-list-group-active-bg: var(--bs-success-text-emphasis);
+ --bs-list-group-active-border-color: var(
+ --bs-success-text-emphasis
+ );
+}
+.list-group-item-info {
+ --bs-list-group-color: var(--bs-info-text-emphasis);
+ --bs-list-group-bg: var(--bs-info-bg-subtle);
+ --bs-list-group-border-color: var(--bs-info-border-subtle);
+ --bs-list-group-action-hover-color: var(--bs-emphasis-color);
+ --bs-list-group-action-hover-bg: var(--bs-info-border-subtle);
+ --bs-list-group-action-active-color: var(--bs-emphasis-color);
+ --bs-list-group-action-active-bg: var(--bs-info-border-subtle);
+ --bs-list-group-active-color: var(--bs-info-bg-subtle);
+ --bs-list-group-active-bg: var(--bs-info-text-emphasis);
+ --bs-list-group-active-border-color: var(
+ --bs-info-text-emphasis
+ );
+}
+.list-group-item-warning {
+ --bs-list-group-color: var(--bs-warning-text-emphasis);
+ --bs-list-group-bg: var(--bs-warning-bg-subtle);
+ --bs-list-group-border-color: var(--bs-warning-border-subtle);
+ --bs-list-group-action-hover-color: var(--bs-emphasis-color);
+ --bs-list-group-action-hover-bg: var(
+ --bs-warning-border-subtle
+ );
+ --bs-list-group-action-active-color: var(--bs-emphasis-color);
+ --bs-list-group-action-active-bg: var(
+ --bs-warning-border-subtle
+ );
+ --bs-list-group-active-color: var(--bs-warning-bg-subtle);
+ --bs-list-group-active-bg: var(--bs-warning-text-emphasis);
+ --bs-list-group-active-border-color: var(
+ --bs-warning-text-emphasis
+ );
+}
+.list-group-item-danger {
+ --bs-list-group-color: var(--bs-danger-text-emphasis);
+ --bs-list-group-bg: var(--bs-danger-bg-subtle);
+ --bs-list-group-border-color: var(--bs-danger-border-subtle);
+ --bs-list-group-action-hover-color: var(--bs-emphasis-color);
+ --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);
+ --bs-list-group-action-active-color: var(--bs-emphasis-color);
+ --bs-list-group-action-active-bg: var(
+ --bs-danger-border-subtle
+ );
+ --bs-list-group-active-color: var(--bs-danger-bg-subtle);
+ --bs-list-group-active-bg: var(--bs-danger-text-emphasis);
+ --bs-list-group-active-border-color: var(
+ --bs-danger-text-emphasis
+ );
+}
+.list-group-item-light {
+ --bs-list-group-color: var(--bs-light-text-emphasis);
+ --bs-list-group-bg: var(--bs-light-bg-subtle);
+ --bs-list-group-border-color: var(--bs-light-border-subtle);
+ --bs-list-group-action-hover-color: var(--bs-emphasis-color);
+ --bs-list-group-action-hover-bg: var(--bs-light-border-subtle);
+ --bs-list-group-action-active-color: var(--bs-emphasis-color);
+ --bs-list-group-action-active-bg: var(--bs-light-border-subtle);
+ --bs-list-group-active-color: var(--bs-light-bg-subtle);
+ --bs-list-group-active-bg: var(--bs-light-text-emphasis);
+ --bs-list-group-active-border-color: var(
+ --bs-light-text-emphasis
+ );
+}
+.list-group-item-dark {
+ --bs-list-group-color: var(--bs-dark-text-emphasis);
+ --bs-list-group-bg: var(--bs-dark-bg-subtle);
+ --bs-list-group-border-color: var(--bs-dark-border-subtle);
+ --bs-list-group-action-hover-color: var(--bs-emphasis-color);
+ --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);
+ --bs-list-group-action-active-color: var(--bs-emphasis-color);
+ --bs-list-group-action-active-bg: var(--bs-dark-border-subtle);
+ --bs-list-group-active-color: var(--bs-dark-bg-subtle);
+ --bs-list-group-active-bg: var(--bs-dark-text-emphasis);
+ --bs-list-group-active-border-color: var(
+ --bs-dark-text-emphasis
+ );
+}
+.btn-close {
+ --bs-btn-close-color: #000;
+ --bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
+ --bs-btn-close-opacity: 0.5;
+ --bs-btn-close-hover-opacity: 0.75;
+ --bs-btn-close-focus-shadow: 0 0 0 0.25rem
+ rgba(13, 110, 253, 0.25);
+ --bs-btn-close-focus-opacity: 1;
+ --bs-btn-close-disabled-opacity: 0.25;
+ --bs-btn-close-white-filter: invert(1) grayscale(100%)
+ brightness(200%);
+ box-sizing: content-box;
+ width: 1em;
+ height: 1em;
+ padding: 0.25em 0.25em;
+ color: var(--bs-btn-close-color);
+ background: transparent var(--bs-btn-close-bg) center/1em auto
+ no-repeat;
+ border: 0;
+ border-radius: 0.375rem;
+ opacity: var(--bs-btn-close-opacity);
+}
+.btn-close:hover {
+ color: var(--bs-btn-close-color);
+ text-decoration: none;
+ opacity: var(--bs-btn-close-hover-opacity);
+}
+.btn-close:focus {
+ outline: 0;
+ box-shadow: var(--bs-btn-close-focus-shadow);
+ opacity: var(--bs-btn-close-focus-opacity);
+}
+.btn-close.disabled,
+.btn-close:disabled {
+ pointer-events: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ opacity: var(--bs-btn-close-disabled-opacity);
+}
+.btn-close-white {
+ filter: var(--bs-btn-close-white-filter);
+}
+[data-bs-theme="dark"] .btn-close {
+ filter: var(--bs-btn-close-white-filter);
+}
+.toast {
+ --bs-toast-zindex: 1090;
+ --bs-toast-padding-x: 0.75rem;
+ --bs-toast-padding-y: 0.5rem;
+ --bs-toast-spacing: 1.5rem;
+ --bs-toast-max-width: 350px;
+ --bs-toast-font-size: 0.875rem;
+ --bs-toast-color: ;
+ --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);
+ --bs-toast-border-width: var(--bs-border-width);
+ --bs-toast-border-color: var(--bs-border-color-translucent);
+ --bs-toast-border-radius: var(--bs-border-radius);
+ --bs-toast-box-shadow: var(--bs-box-shadow);
+ --bs-toast-header-color: var(--bs-secondary-color);
+ --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85);
+ --bs-toast-header-border-color: var(
+ --bs-border-color-translucent
+ );
+ width: var(--bs-toast-max-width);
+ max-width: 100%;
+ font-size: var(--bs-toast-font-size);
+ color: var(--bs-toast-color);
+ pointer-events: auto;
+ background-color: var(--bs-toast-bg);
+ background-clip: padding-box;
+ border: var(--bs-toast-border-width) solid
+ var(--bs-toast-border-color);
+ box-shadow: var(--bs-toast-box-shadow);
+ border-radius: var(--bs-toast-border-radius);
+}
+.toast.showing {
+ opacity: 0;
+}
+.toast:not(.show) {
+ display: none;
+}
+.toast-container {
+ --bs-toast-zindex: 1090;
+ position: absolute;
+ z-index: var(--bs-toast-zindex);
+ width: -webkit-max-content;
+ width: -moz-max-content;
+ width: max-content;
+ max-width: 100%;
+ pointer-events: none;
+}
+.toast-container > :not(:last-child) {
+ margin-bottom: var(--bs-toast-spacing);
+}
+.toast-header {
+ display: flex;
+ align-items: center;
+ padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);
+ color: var(--bs-toast-header-color);
+ background-color: var(--bs-toast-header-bg);
+ background-clip: padding-box;
+ border-bottom: var(--bs-toast-border-width) solid
+ var(--bs-toast-header-border-color);
+ border-top-left-radius: calc(
+ var(--bs-toast-border-radius) - var(--bs-toast-border-width)
+ );
+ border-top-right-radius: calc(
+ var(--bs-toast-border-radius) - var(--bs-toast-border-width)
+ );
+}
+.toast-header .btn-close {
+ margin-right: calc(-0.5 * var(--bs-toast-padding-x));
+ margin-left: var(--bs-toast-padding-x);
+}
+.toast-body {
+ padding: var(--bs-toast-padding-x);
+ word-wrap: break-word;
+}
+.modal {
+ --bs-modal-zindex: 1055;
+ --bs-modal-width: 500px;
+ --bs-modal-padding: 1rem;
+ --bs-modal-margin: 0.5rem;
+ --bs-modal-color: ;
+ --bs-modal-bg: var(--bs-body-bg);
+ --bs-modal-border-color: var(--bs-border-color-translucent);
+ --bs-modal-border-width: var(--bs-border-width);
+ --bs-modal-border-radius: var(--bs-border-radius-lg);
+ --bs-modal-box-shadow: var(--bs-box-shadow-sm);
+ --bs-modal-inner-border-radius: calc(
+ var(--bs-border-radius-lg) - (var(--bs-border-width))
+ );
+ --bs-modal-header-padding-x: 1rem;
+ --bs-modal-header-padding-y: 1rem;
+ --bs-modal-header-padding: 1rem 1rem;
+ --bs-modal-header-border-color: var(--bs-border-color);
+ --bs-modal-header-border-width: var(--bs-border-width);
+ --bs-modal-title-line-height: 1.5;
+ --bs-modal-footer-gap: 0.5rem;
+ --bs-modal-footer-bg: ;
+ --bs-modal-footer-border-color: var(--bs-border-color);
+ --bs-modal-footer-border-width: var(--bs-border-width);
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: var(--bs-modal-zindex);
+ display: none;
+ width: 100%;
+ height: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
+ outline: 0;
+}
+.modal-dialog {
+ position: relative;
+ width: auto;
+ margin: var(--bs-modal-margin);
+ pointer-events: none;
+}
+.modal.fade .modal-dialog {
+ transition: transform 0.3s ease-out;
+ transform: translate(0, -50px);
+}
+@media (prefers-reduced-motion: reduce) {
+ .modal.fade .modal-dialog {
+ transition: none;
+ }
+}
+.modal.show .modal-dialog {
+ transform: none;
+}
+.modal.modal-static .modal-dialog {
+ transform: scale(1.02);
+}
+.modal-dialog-scrollable {
+ height: calc(100% - var(--bs-modal-margin) * 2);
+}
+.modal-dialog-scrollable .modal-content {
+ max-height: 100%;
+ overflow: hidden;
+}
+.modal-dialog-scrollable .modal-body {
+ overflow-y: auto;
+}
+.modal-dialog-centered {
+ display: flex;
+ align-items: center;
+ min-height: calc(100% - var(--bs-modal-margin) * 2);
+}
+.modal-content {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ color: var(--bs-modal-color);
+ pointer-events: auto;
+ background-color: var(--bs-modal-bg);
+ background-clip: padding-box;
+ border: var(--bs-modal-border-width) solid
+ var(--bs-modal-border-color);
+ border-radius: var(--bs-modal-border-radius);
+ outline: 0;
+}
+.modal-backdrop {
+ --bs-backdrop-zindex: 1050;
+ --bs-backdrop-bg: #000;
+ --bs-backdrop-opacity: 0.5;
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: var(--bs-backdrop-zindex);
+ width: 100vw;
+ height: 100vh;
+ background-color: var(--bs-backdrop-bg);
+}
+.modal-backdrop.fade {
+ opacity: 0;
+}
+.modal-backdrop.show {
+ opacity: var(--bs-backdrop-opacity);
+}
+.modal-header {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--bs-modal-header-padding);
+ border-bottom: var(--bs-modal-header-border-width) solid
+ var(--bs-modal-header-border-color);
+ border-top-left-radius: var(--bs-modal-inner-border-radius);
+ border-top-right-radius: var(--bs-modal-inner-border-radius);
+}
+.modal-header .btn-close {
+ padding: calc(var(--bs-modal-header-padding-y) * 0.5)
+ calc(var(--bs-modal-header-padding-x) * 0.5);
+ margin: calc(-0.5 * var(--bs-modal-header-padding-y))
+ calc(-0.5 * var(--bs-modal-header-padding-x))
+ calc(-0.5 * var(--bs-modal-header-padding-y)) auto;
+}
+.modal-title {
+ margin-bottom: 0;
+ line-height: var(--bs-modal-title-line-height);
+}
+.modal-body {
+ position: relative;
+ flex: 1 1 auto;
+ padding: var(--bs-modal-padding);
+}
+.modal-footer {
+ display: flex;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-end;
+ padding: calc(
+ var(--bs-modal-padding) - var(--bs-modal-footer-gap) * 0.5
+ );
+ background-color: var(--bs-modal-footer-bg);
+ border-top: var(--bs-modal-footer-border-width) solid
+ var(--bs-modal-footer-border-color);
+ border-bottom-right-radius: var(--bs-modal-inner-border-radius);
+ border-bottom-left-radius: var(--bs-modal-inner-border-radius);
+}
+.modal-footer > * {
+ margin: calc(var(--bs-modal-footer-gap) * 0.5);
+}
+@media (min-width: 576px) {
+ .modal {
+ --bs-modal-margin: 1.75rem;
+ --bs-modal-box-shadow: var(--bs-box-shadow);
+ }
+ .modal-dialog {
+ max-width: var(--bs-modal-width);
+ margin-right: auto;
+ margin-left: auto;
+ }
+ .modal-sm {
+ --bs-modal-width: 300px;
+ }
+}
+@media (min-width: 992px) {
+ .modal-lg,
+ .modal-xl {
+ --bs-modal-width: 800px;
+ }
+}
+@media (min-width: 1200px) {
+ .modal-xl {
+ --bs-modal-width: 1140px;
+ }
+}
+.modal-fullscreen {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+}
+.modal-fullscreen .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+}
+.modal-fullscreen .modal-footer,
+.modal-fullscreen .modal-header {
+ border-radius: 0;
+}
+.modal-fullscreen .modal-body {
+ overflow-y: auto;
+}
+@media (max-width: 575.98px) {
+ .modal-fullscreen-sm-down {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+ }
+ .modal-fullscreen-sm-down .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+ }
+ .modal-fullscreen-sm-down .modal-footer,
+ .modal-fullscreen-sm-down .modal-header {
+ border-radius: 0;
+ }
+ .modal-fullscreen-sm-down .modal-body {
+ overflow-y: auto;
+ }
+}
+@media (max-width: 767.98px) {
+ .modal-fullscreen-md-down {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+ }
+ .modal-fullscreen-md-down .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+ }
+ .modal-fullscreen-md-down .modal-footer,
+ .modal-fullscreen-md-down .modal-header {
+ border-radius: 0;
+ }
+ .modal-fullscreen-md-down .modal-body {
+ overflow-y: auto;
+ }
+}
+@media (max-width: 991.98px) {
+ .modal-fullscreen-lg-down {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+ }
+ .modal-fullscreen-lg-down .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+ }
+ .modal-fullscreen-lg-down .modal-footer,
+ .modal-fullscreen-lg-down .modal-header {
+ border-radius: 0;
+ }
+ .modal-fullscreen-lg-down .modal-body {
+ overflow-y: auto;
+ }
+}
+@media (max-width: 1199.98px) {
+ .modal-fullscreen-xl-down {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+ }
+ .modal-fullscreen-xl-down .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+ }
+ .modal-fullscreen-xl-down .modal-footer,
+ .modal-fullscreen-xl-down .modal-header {
+ border-radius: 0;
+ }
+ .modal-fullscreen-xl-down .modal-body {
+ overflow-y: auto;
+ }
+}
+@media (max-width: 1399.98px) {
+ .modal-fullscreen-xxl-down {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+ }
+ .modal-fullscreen-xxl-down .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+ }
+ .modal-fullscreen-xxl-down .modal-footer,
+ .modal-fullscreen-xxl-down .modal-header {
+ border-radius: 0;
+ }
+ .modal-fullscreen-xxl-down .modal-body {
+ overflow-y: auto;
+ }
+}
+.tooltip {
+ --bs-tooltip-zindex: 1080;
+ --bs-tooltip-max-width: 200px;
+ --bs-tooltip-padding-x: 0.5rem;
+ --bs-tooltip-padding-y: 0.25rem;
+ --bs-tooltip-margin: ;
+ --bs-tooltip-font-size: 0.875rem;
+ --bs-tooltip-color: var(--bs-body-bg);
+ --bs-tooltip-bg: var(--bs-emphasis-color);
+ --bs-tooltip-border-radius: var(--bs-border-radius);
+ --bs-tooltip-opacity: 0.9;
+ --bs-tooltip-arrow-width: 0.8rem;
+ --bs-tooltip-arrow-height: 0.4rem;
+ z-index: var(--bs-tooltip-zindex);
+ display: block;
+ margin: var(--bs-tooltip-margin);
+ font-family: var(--bs-font-sans-serif);
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.5;
+ text-align: left;
+ text-align: start;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ letter-spacing: normal;
+ word-break: normal;
+ white-space: normal;
+ word-spacing: normal;
+ line-break: auto;
+ font-size: var(--bs-tooltip-font-size);
+ word-wrap: break-word;
+ opacity: 0;
+}
+.tooltip.show {
+ opacity: var(--bs-tooltip-opacity);
+}
+.tooltip .tooltip-arrow {
+ display: block;
+ width: var(--bs-tooltip-arrow-width);
+ height: var(--bs-tooltip-arrow-height);
+}
+.tooltip .tooltip-arrow::before {
+ position: absolute;
+ content: "";
+ border-color: transparent;
+ border-style: solid;
+}
+.bs-tooltip-auto[data-popper-placement^="top"] .tooltip-arrow,
+.bs-tooltip-top .tooltip-arrow {
+ bottom: calc(-1 * var(--bs-tooltip-arrow-height));
+}
+.bs-tooltip-auto[data-popper-placement^="top"]
+ .tooltip-arrow::before,
+.bs-tooltip-top .tooltip-arrow::before {
+ top: -1px;
+ border-width: var(--bs-tooltip-arrow-height)
+ calc(var(--bs-tooltip-arrow-width) * 0.5) 0;
+ border-top-color: var(--bs-tooltip-bg);
+}
+.bs-tooltip-auto[data-popper-placement^="right"] .tooltip-arrow,
+.bs-tooltip-end .tooltip-arrow {
+ left: calc(-1 * var(--bs-tooltip-arrow-height));
+ width: var(--bs-tooltip-arrow-height);
+ height: var(--bs-tooltip-arrow-width);
+}
+.bs-tooltip-auto[data-popper-placement^="right"]
+ .tooltip-arrow::before,
+.bs-tooltip-end .tooltip-arrow::before {
+ right: -1px;
+ border-width: calc(var(--bs-tooltip-arrow-width) * 0.5)
+ var(--bs-tooltip-arrow-height)
+ calc(var(--bs-tooltip-arrow-width) * 0.5) 0;
+ border-right-color: var(--bs-tooltip-bg);
+}
+.bs-tooltip-auto[data-popper-placement^="bottom"] .tooltip-arrow,
+.bs-tooltip-bottom .tooltip-arrow {
+ top: calc(-1 * var(--bs-tooltip-arrow-height));
+}
+.bs-tooltip-auto[data-popper-placement^="bottom"]
+ .tooltip-arrow::before,
+.bs-tooltip-bottom .tooltip-arrow::before {
+ bottom: -1px;
+ border-width: 0 calc(var(--bs-tooltip-arrow-width) * 0.5)
+ var(--bs-tooltip-arrow-height);
+ border-bottom-color: var(--bs-tooltip-bg);
+}
+.bs-tooltip-auto[data-popper-placement^="left"] .tooltip-arrow,
+.bs-tooltip-start .tooltip-arrow {
+ right: calc(-1 * var(--bs-tooltip-arrow-height));
+ width: var(--bs-tooltip-arrow-height);
+ height: var(--bs-tooltip-arrow-width);
+}
+.bs-tooltip-auto[data-popper-placement^="left"]
+ .tooltip-arrow::before,
+.bs-tooltip-start .tooltip-arrow::before {
+ left: -1px;
+ border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) 0
+ calc(var(--bs-tooltip-arrow-width) * 0.5)
+ var(--bs-tooltip-arrow-height);
+ border-left-color: var(--bs-tooltip-bg);
+}
+.tooltip-inner {
+ max-width: var(--bs-tooltip-max-width);
+ padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);
+ color: var(--bs-tooltip-color);
+ text-align: center;
+ background-color: var(--bs-tooltip-bg);
+ border-radius: var(--bs-tooltip-border-radius);
+}
+.popover {
+ --bs-popover-zindex: 1070;
+ --bs-popover-max-width: 276px;
+ --bs-popover-font-size: 0.875rem;
+ --bs-popover-bg: var(--bs-body-bg);
+ --bs-popover-border-width: var(--bs-border-width);
+ --bs-popover-border-color: var(--bs-border-color-translucent);
+ --bs-popover-border-radius: var(--bs-border-radius-lg);
+ --bs-popover-inner-border-radius: calc(
+ var(--bs-border-radius-lg) - var(--bs-border-width)
+ );
+ --bs-popover-box-shadow: var(--bs-box-shadow);
+ --bs-popover-header-padding-x: 1rem;
+ --bs-popover-header-padding-y: 0.5rem;
+ --bs-popover-header-font-size: 1rem;
+ --bs-popover-header-color: inherit;
+ --bs-popover-header-bg: var(--bs-secondary-bg);
+ --bs-popover-body-padding-x: 1rem;
+ --bs-popover-body-padding-y: 1rem;
+ --bs-popover-body-color: var(--bs-body-color);
+ --bs-popover-arrow-width: 1rem;
+ --bs-popover-arrow-height: 0.5rem;
+ --bs-popover-arrow-border: var(--bs-popover-border-color);
+ z-index: var(--bs-popover-zindex);
+ display: block;
+ max-width: var(--bs-popover-max-width);
+ font-family: var(--bs-font-sans-serif);
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.5;
+ text-align: left;
+ text-align: start;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ letter-spacing: normal;
+ word-break: normal;
+ white-space: normal;
+ word-spacing: normal;
+ line-break: auto;
+ font-size: var(--bs-popover-font-size);
+ word-wrap: break-word;
+ background-color: var(--bs-popover-bg);
+ background-clip: padding-box;
+ border: var(--bs-popover-border-width) solid
+ var(--bs-popover-border-color);
+ border-radius: var(--bs-popover-border-radius);
+}
+.popover .popover-arrow {
+ display: block;
+ width: var(--bs-popover-arrow-width);
+ height: var(--bs-popover-arrow-height);
+}
+.popover .popover-arrow::after,
+.popover .popover-arrow::before {
+ position: absolute;
+ display: block;
+ content: "";
+ border-color: transparent;
+ border-style: solid;
+ border-width: 0;
+}
+.bs-popover-auto[data-popper-placement^="top"] > .popover-arrow,
+.bs-popover-top > .popover-arrow {
+ bottom: calc(
+ -1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)
+ );
+}
+.bs-popover-auto[data-popper-placement^="top"]
+ > .popover-arrow::after,
+.bs-popover-auto[data-popper-placement^="top"]
+ > .popover-arrow::before,
+.bs-popover-top > .popover-arrow::after,
+.bs-popover-top > .popover-arrow::before {
+ border-width: var(--bs-popover-arrow-height)
+ calc(var(--bs-popover-arrow-width) * 0.5) 0;
+}
+.bs-popover-auto[data-popper-placement^="top"]
+ > .popover-arrow::before,
+.bs-popover-top > .popover-arrow::before {
+ bottom: 0;
+ border-top-color: var(--bs-popover-arrow-border);
+}
+.bs-popover-auto[data-popper-placement^="top"]
+ > .popover-arrow::after,
+.bs-popover-top > .popover-arrow::after {
+ bottom: var(--bs-popover-border-width);
+ border-top-color: var(--bs-popover-bg);
+}
+.bs-popover-auto[data-popper-placement^="right"] > .popover-arrow,
+.bs-popover-end > .popover-arrow {
+ left: calc(
+ -1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)
+ );
+ width: var(--bs-popover-arrow-height);
+ height: var(--bs-popover-arrow-width);
+}
+.bs-popover-auto[data-popper-placement^="right"]
+ > .popover-arrow::after,
+.bs-popover-auto[data-popper-placement^="right"]
+ > .popover-arrow::before,
+.bs-popover-end > .popover-arrow::after,
+.bs-popover-end > .popover-arrow::before {
+ border-width: calc(var(--bs-popover-arrow-width) * 0.5)
+ var(--bs-popover-arrow-height)
+ calc(var(--bs-popover-arrow-width) * 0.5) 0;
+}
+.bs-popover-auto[data-popper-placement^="right"]
+ > .popover-arrow::before,
+.bs-popover-end > .popover-arrow::before {
+ left: 0;
+ border-right-color: var(--bs-popover-arrow-border);
+}
+.bs-popover-auto[data-popper-placement^="right"]
+ > .popover-arrow::after,
+.bs-popover-end > .popover-arrow::after {
+ left: var(--bs-popover-border-width);
+ border-right-color: var(--bs-popover-bg);
+}
+.bs-popover-auto[data-popper-placement^="bottom"] > .popover-arrow,
+.bs-popover-bottom > .popover-arrow {
+ top: calc(
+ -1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)
+ );
+}
+.bs-popover-auto[data-popper-placement^="bottom"]
+ > .popover-arrow::after,
+.bs-popover-auto[data-popper-placement^="bottom"]
+ > .popover-arrow::before,
+.bs-popover-bottom > .popover-arrow::after,
+.bs-popover-bottom > .popover-arrow::before {
+ border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5)
+ var(--bs-popover-arrow-height);
+}
+.bs-popover-auto[data-popper-placement^="bottom"]
+ > .popover-arrow::before,
+.bs-popover-bottom > .popover-arrow::before {
+ top: 0;
+ border-bottom-color: var(--bs-popover-arrow-border);
+}
+.bs-popover-auto[data-popper-placement^="bottom"]
+ > .popover-arrow::after,
+.bs-popover-bottom > .popover-arrow::after {
+ top: var(--bs-popover-border-width);
+ border-bottom-color: var(--bs-popover-bg);
+}
+.bs-popover-auto[data-popper-placement^="bottom"]
+ .popover-header::before,
+.bs-popover-bottom .popover-header::before {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ display: block;
+ width: var(--bs-popover-arrow-width);
+ margin-left: calc(-0.5 * var(--bs-popover-arrow-width));
+ content: "";
+ border-bottom: var(--bs-popover-border-width) solid
+ var(--bs-popover-header-bg);
+}
+.bs-popover-auto[data-popper-placement^="left"] > .popover-arrow,
+.bs-popover-start > .popover-arrow {
+ right: calc(
+ -1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)
+ );
+ width: var(--bs-popover-arrow-height);
+ height: var(--bs-popover-arrow-width);
+}
+.bs-popover-auto[data-popper-placement^="left"]
+ > .popover-arrow::after,
+.bs-popover-auto[data-popper-placement^="left"]
+ > .popover-arrow::before,
+.bs-popover-start > .popover-arrow::after,
+.bs-popover-start > .popover-arrow::before {
+ border-width: calc(var(--bs-popover-arrow-width) * 0.5) 0
+ calc(var(--bs-popover-arrow-width) * 0.5)
+ var(--bs-popover-arrow-height);
+}
+.bs-popover-auto[data-popper-placement^="left"]
+ > .popover-arrow::before,
+.bs-popover-start > .popover-arrow::before {
+ right: 0;
+ border-left-color: var(--bs-popover-arrow-border);
+}
+.bs-popover-auto[data-popper-placement^="left"]
+ > .popover-arrow::after,
+.bs-popover-start > .popover-arrow::after {
+ right: var(--bs-popover-border-width);
+ border-left-color: var(--bs-popover-bg);
+}
+.popover-header {
+ padding: var(--bs-popover-header-padding-y)
+ var(--bs-popover-header-padding-x);
+ margin-bottom: 0;
+ font-size: var(--bs-popover-header-font-size);
+ color: var(--bs-popover-header-color);
+ background-color: var(--bs-popover-header-bg);
+ border-bottom: var(--bs-popover-border-width) solid
+ var(--bs-popover-border-color);
+ border-top-left-radius: var(--bs-popover-inner-border-radius);
+ border-top-right-radius: var(--bs-popover-inner-border-radius);
+}
+.popover-header:empty {
+ display: none;
+}
+.popover-body {
+ padding: var(--bs-popover-body-padding-y)
+ var(--bs-popover-body-padding-x);
+ color: var(--bs-popover-body-color);
+}
+.carousel {
+ position: relative;
+}
+.carousel.pointer-event {
+ touch-action: pan-y;
+}
+.carousel-inner {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+}
+.carousel-inner::after {
+ display: block;
+ clear: both;
+ content: "";
+}
+.carousel-item {
+ position: relative;
+ display: none;
+ float: left;
+ width: 100%;
+ margin-right: -100%;
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ transition: transform 0.6s ease-in-out;
+}
+@media (prefers-reduced-motion: reduce) {
+ .carousel-item {
+ transition: none;
+ }
+}
+.carousel-item-next,
+.carousel-item-prev,
+.carousel-item.active {
+ display: block;
+}
+.active.carousel-item-end,
+.carousel-item-next:not(.carousel-item-start) {
+ transform: translateX(100%);
+}
+.active.carousel-item-start,
+.carousel-item-prev:not(.carousel-item-end) {
+ transform: translateX(-100%);
+}
+.carousel-fade .carousel-item {
+ opacity: 0;
+ transition-property: opacity;
+ transform: none;
+}
+.carousel-fade .carousel-item-next.carousel-item-start,
+.carousel-fade .carousel-item-prev.carousel-item-end,
+.carousel-fade .carousel-item.active {
+ z-index: 1;
+ opacity: 1;
+}
+.carousel-fade .active.carousel-item-end,
+.carousel-fade .active.carousel-item-start {
+ z-index: 0;
+ opacity: 0;
+ transition: opacity 0s 0.6s;
+}
+@media (prefers-reduced-motion: reduce) {
+ .carousel-fade .active.carousel-item-end,
+ .carousel-fade .active.carousel-item-start {
+ transition: none;
+ }
+}
+.carousel-control-next,
+.carousel-control-prev {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 15%;
+ padding: 0;
+ color: #fff;
+ text-align: center;
+ background: 0 0;
+ border: 0;
+ opacity: 0.5;
+ transition: opacity 0.15s ease;
+}
+@media (prefers-reduced-motion: reduce) {
+ .carousel-control-next,
+ .carousel-control-prev {
+ transition: none;
+ }
+}
+.carousel-control-next:focus,
+.carousel-control-next:hover,
+.carousel-control-prev:focus,
+.carousel-control-prev:hover {
+ color: #fff;
+ text-decoration: none;
+ outline: 0;
+ opacity: 0.9;
+}
+.carousel-control-prev {
+ left: 0;
+}
+.carousel-control-next {
+ right: 0;
+}
+.carousel-control-next-icon,
+.carousel-control-prev-icon {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ background-repeat: no-repeat;
+ background-position: 50%;
+ background-size: 100% 100%;
+}
+.carousel-control-prev-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e");
+}
+.carousel-control-next-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+}
+.carousel-indicators {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 2;
+ display: flex;
+ justify-content: center;
+ padding: 0;
+ margin-right: 15%;
+ margin-bottom: 1rem;
+ margin-left: 15%;
+}
+.carousel-indicators [data-bs-target] {
+ box-sizing: content-box;
+ flex: 0 1 auto;
+ width: 30px;
+ height: 3px;
+ padding: 0;
+ margin-right: 3px;
+ margin-left: 3px;
+ text-indent: -999px;
+ cursor: pointer;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 0;
+ border-top: 10px solid transparent;
+ border-bottom: 10px solid transparent;
+ opacity: 0.5;
+ transition: opacity 0.6s ease;
+}
+@media (prefers-reduced-motion: reduce) {
+ .carousel-indicators [data-bs-target] {
+ transition: none;
+ }
+}
+.carousel-indicators .active {
+ opacity: 1;
+}
+.carousel-caption {
+ position: absolute;
+ right: 15%;
+ bottom: 1.25rem;
+ left: 15%;
+ padding-top: 1.25rem;
+ padding-bottom: 1.25rem;
+ color: #fff;
+ text-align: center;
+}
+.carousel-dark .carousel-control-next-icon,
+.carousel-dark .carousel-control-prev-icon {
+ filter: invert(1) grayscale(100);
+}
+.carousel-dark .carousel-indicators [data-bs-target] {
+ background-color: #000;
+}
+.carousel-dark .carousel-caption {
+ color: #000;
+}
+[data-bs-theme="dark"] .carousel .carousel-control-next-icon,
+[data-bs-theme="dark"] .carousel .carousel-control-prev-icon,
+[data-bs-theme="dark"].carousel .carousel-control-next-icon,
+[data-bs-theme="dark"].carousel .carousel-control-prev-icon {
+ filter: invert(1) grayscale(100);
+}
+[data-bs-theme="dark"]
+ .carousel
+ .carousel-indicators
+ [data-bs-target],
+[data-bs-theme="dark"].carousel
+ .carousel-indicators
+ [data-bs-target] {
+ background-color: #000;
+}
+[data-bs-theme="dark"] .carousel .carousel-caption,
+[data-bs-theme="dark"].carousel .carousel-caption {
+ color: #000;
+}
+.spinner-border,
+.spinner-grow {
+ display: inline-block;
+ width: var(--bs-spinner-width);
+ height: var(--bs-spinner-height);
+ vertical-align: var(--bs-spinner-vertical-align);
+ border-radius: 50%;
+ animation: var(--bs-spinner-animation-speed) linear infinite
+ var(--bs-spinner-animation-name);
+}
+@keyframes spinner-border {
+ to {
+ transform: rotate(360deg);
+ }
+}
+.spinner-border {
+ --bs-spinner-width: 2rem;
+ --bs-spinner-height: 2rem;
+ --bs-spinner-vertical-align: -0.125em;
+ --bs-spinner-border-width: 0.25em;
+ --bs-spinner-animation-speed: 0.75s;
+ --bs-spinner-animation-name: spinner-border;
+ border: var(--bs-spinner-border-width) solid currentcolor;
+ border-right-color: transparent;
+}
+.spinner-border-sm {
+ --bs-spinner-width: 1rem;
+ --bs-spinner-height: 1rem;
+ --bs-spinner-border-width: 0.2em;
+}
+@keyframes spinner-grow {
+ 0% {
+ transform: scale(0);
+ }
+ 50% {
+ opacity: 1;
+ transform: none;
+ }
+}
+.spinner-grow {
+ --bs-spinner-width: 2rem;
+ --bs-spinner-height: 2rem;
+ --bs-spinner-vertical-align: -0.125em;
+ --bs-spinner-animation-speed: 0.75s;
+ --bs-spinner-animation-name: spinner-grow;
+ background-color: currentcolor;
+ opacity: 0;
+}
+.spinner-grow-sm {
+ --bs-spinner-width: 1rem;
+ --bs-spinner-height: 1rem;
+}
+@media (prefers-reduced-motion: reduce) {
+ .spinner-border,
+ .spinner-grow {
+ --bs-spinner-animation-speed: 1.5s;
+ }
+}
+.offcanvas,
+.offcanvas-lg,
+.offcanvas-md,
+.offcanvas-sm,
+.offcanvas-xl,
+.offcanvas-xxl {
+ --bs-offcanvas-zindex: 1045;
+ --bs-offcanvas-width: 400px;
+ --bs-offcanvas-height: 30vh;
+ --bs-offcanvas-padding-x: 1rem;
+ --bs-offcanvas-padding-y: 1rem;
+ --bs-offcanvas-color: var(--bs-body-color);
+ --bs-offcanvas-bg: var(--bs-body-bg);
+ --bs-offcanvas-border-width: var(--bs-border-width);
+ --bs-offcanvas-border-color: var(--bs-border-color-translucent);
+ --bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);
+ --bs-offcanvas-transition: transform 0.3s ease-in-out;
+ --bs-offcanvas-title-line-height: 1.5;
+}
+@media (max-width: 575.98px) {
+ .offcanvas-sm {
+ position: fixed;
+ bottom: 0;
+ z-index: var(--bs-offcanvas-zindex);
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ color: var(--bs-offcanvas-color);
+ visibility: hidden;
+ background-color: var(--bs-offcanvas-bg);
+ background-clip: padding-box;
+ outline: 0;
+ transition: var(--bs-offcanvas-transition);
+ }
+}
+@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) {
+ .offcanvas-sm {
+ transition: none;
+ }
+}
+@media (max-width: 575.98px) {
+ .offcanvas-sm.offcanvas-start {
+ top: 0;
+ left: 0;
+ width: var(--bs-offcanvas-width);
+ border-right: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(-100%);
+ }
+ .offcanvas-sm.offcanvas-end {
+ top: 0;
+ right: 0;
+ width: var(--bs-offcanvas-width);
+ border-left: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(100%);
+ }
+ .offcanvas-sm.offcanvas-top {
+ top: 0;
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-bottom: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(-100%);
+ }
+ .offcanvas-sm.offcanvas-bottom {
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-top: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(100%);
+ }
+ .offcanvas-sm.show:not(.hiding),
+ .offcanvas-sm.showing {
+ transform: none;
+ }
+ .offcanvas-sm.hiding,
+ .offcanvas-sm.show,
+ .offcanvas-sm.showing {
+ visibility: visible;
+ }
+}
+@media (min-width: 576px) {
+ .offcanvas-sm {
+ --bs-offcanvas-height: auto;
+ --bs-offcanvas-border-width: 0;
+ background-color: transparent !important;
+ }
+ .offcanvas-sm .offcanvas-header {
+ display: none;
+ }
+ .offcanvas-sm .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ background-color: transparent !important;
+ }
+}
+@media (max-width: 767.98px) {
+ .offcanvas-md {
+ position: fixed;
+ bottom: 0;
+ z-index: var(--bs-offcanvas-zindex);
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ color: var(--bs-offcanvas-color);
+ visibility: hidden;
+ background-color: var(--bs-offcanvas-bg);
+ background-clip: padding-box;
+ outline: 0;
+ transition: var(--bs-offcanvas-transition);
+ }
+}
+@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) {
+ .offcanvas-md {
+ transition: none;
+ }
+}
+@media (max-width: 767.98px) {
+ .offcanvas-md.offcanvas-start {
+ top: 0;
+ left: 0;
+ width: var(--bs-offcanvas-width);
+ border-right: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(-100%);
+ }
+ .offcanvas-md.offcanvas-end {
+ top: 0;
+ right: 0;
+ width: var(--bs-offcanvas-width);
+ border-left: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(100%);
+ }
+ .offcanvas-md.offcanvas-top {
+ top: 0;
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-bottom: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(-100%);
+ }
+ .offcanvas-md.offcanvas-bottom {
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-top: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(100%);
+ }
+ .offcanvas-md.show:not(.hiding),
+ .offcanvas-md.showing {
+ transform: none;
+ }
+ .offcanvas-md.hiding,
+ .offcanvas-md.show,
+ .offcanvas-md.showing {
+ visibility: visible;
+ }
+}
+@media (min-width: 768px) {
+ .offcanvas-md {
+ --bs-offcanvas-height: auto;
+ --bs-offcanvas-border-width: 0;
+ background-color: transparent !important;
+ }
+ .offcanvas-md .offcanvas-header {
+ display: none;
+ }
+ .offcanvas-md .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ background-color: transparent !important;
+ }
+}
+@media (max-width: 991.98px) {
+ .offcanvas-lg {
+ position: fixed;
+ bottom: 0;
+ z-index: var(--bs-offcanvas-zindex);
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ color: var(--bs-offcanvas-color);
+ visibility: hidden;
+ background-color: var(--bs-offcanvas-bg);
+ background-clip: padding-box;
+ outline: 0;
+ transition: var(--bs-offcanvas-transition);
+ }
+}
+@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) {
+ .offcanvas-lg {
+ transition: none;
+ }
+}
+@media (max-width: 991.98px) {
+ .offcanvas-lg.offcanvas-start {
+ top: 0;
+ left: 0;
+ width: var(--bs-offcanvas-width);
+ border-right: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(-100%);
+ }
+ .offcanvas-lg.offcanvas-end {
+ top: 0;
+ right: 0;
+ width: var(--bs-offcanvas-width);
+ border-left: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(100%);
+ }
+ .offcanvas-lg.offcanvas-top {
+ top: 0;
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-bottom: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(-100%);
+ }
+ .offcanvas-lg.offcanvas-bottom {
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-top: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(100%);
+ }
+ .offcanvas-lg.show:not(.hiding),
+ .offcanvas-lg.showing {
+ transform: none;
+ }
+ .offcanvas-lg.hiding,
+ .offcanvas-lg.show,
+ .offcanvas-lg.showing {
+ visibility: visible;
+ }
+}
+@media (min-width: 992px) {
+ .offcanvas-lg {
+ --bs-offcanvas-height: auto;
+ --bs-offcanvas-border-width: 0;
+ background-color: transparent !important;
+ }
+ .offcanvas-lg .offcanvas-header {
+ display: none;
+ }
+ .offcanvas-lg .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ background-color: transparent !important;
+ }
+}
+@media (max-width: 1199.98px) {
+ .offcanvas-xl {
+ position: fixed;
+ bottom: 0;
+ z-index: var(--bs-offcanvas-zindex);
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ color: var(--bs-offcanvas-color);
+ visibility: hidden;
+ background-color: var(--bs-offcanvas-bg);
+ background-clip: padding-box;
+ outline: 0;
+ transition: var(--bs-offcanvas-transition);
+ }
+}
+@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) {
+ .offcanvas-xl {
+ transition: none;
+ }
+}
+@media (max-width: 1199.98px) {
+ .offcanvas-xl.offcanvas-start {
+ top: 0;
+ left: 0;
+ width: var(--bs-offcanvas-width);
+ border-right: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(-100%);
+ }
+ .offcanvas-xl.offcanvas-end {
+ top: 0;
+ right: 0;
+ width: var(--bs-offcanvas-width);
+ border-left: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(100%);
+ }
+ .offcanvas-xl.offcanvas-top {
+ top: 0;
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-bottom: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(-100%);
+ }
+ .offcanvas-xl.offcanvas-bottom {
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-top: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(100%);
+ }
+ .offcanvas-xl.show:not(.hiding),
+ .offcanvas-xl.showing {
+ transform: none;
+ }
+ .offcanvas-xl.hiding,
+ .offcanvas-xl.show,
+ .offcanvas-xl.showing {
+ visibility: visible;
+ }
+}
+@media (min-width: 1200px) {
+ .offcanvas-xl {
+ --bs-offcanvas-height: auto;
+ --bs-offcanvas-border-width: 0;
+ background-color: transparent !important;
+ }
+ .offcanvas-xl .offcanvas-header {
+ display: none;
+ }
+ .offcanvas-xl .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ background-color: transparent !important;
+ }
+}
+@media (max-width: 1399.98px) {
+ .offcanvas-xxl {
+ position: fixed;
+ bottom: 0;
+ z-index: var(--bs-offcanvas-zindex);
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ color: var(--bs-offcanvas-color);
+ visibility: hidden;
+ background-color: var(--bs-offcanvas-bg);
+ background-clip: padding-box;
+ outline: 0;
+ transition: var(--bs-offcanvas-transition);
+ }
+}
+@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) {
+ .offcanvas-xxl {
+ transition: none;
+ }
+}
+@media (max-width: 1399.98px) {
+ .offcanvas-xxl.offcanvas-start {
+ top: 0;
+ left: 0;
+ width: var(--bs-offcanvas-width);
+ border-right: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(-100%);
+ }
+ .offcanvas-xxl.offcanvas-end {
+ top: 0;
+ right: 0;
+ width: var(--bs-offcanvas-width);
+ border-left: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(100%);
+ }
+ .offcanvas-xxl.offcanvas-top {
+ top: 0;
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-bottom: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(-100%);
+ }
+ .offcanvas-xxl.offcanvas-bottom {
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-top: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(100%);
+ }
+ .offcanvas-xxl.show:not(.hiding),
+ .offcanvas-xxl.showing {
+ transform: none;
+ }
+ .offcanvas-xxl.hiding,
+ .offcanvas-xxl.show,
+ .offcanvas-xxl.showing {
+ visibility: visible;
+ }
+}
+@media (min-width: 1400px) {
+ .offcanvas-xxl {
+ --bs-offcanvas-height: auto;
+ --bs-offcanvas-border-width: 0;
+ background-color: transparent !important;
+ }
+ .offcanvas-xxl .offcanvas-header {
+ display: none;
+ }
+ .offcanvas-xxl .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ background-color: transparent !important;
+ }
+}
+.offcanvas {
+ position: fixed;
+ bottom: 0;
+ z-index: var(--bs-offcanvas-zindex);
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ color: var(--bs-offcanvas-color);
+ visibility: hidden;
+ background-color: var(--bs-offcanvas-bg);
+ background-clip: padding-box;
+ outline: 0;
+ transition: var(--bs-offcanvas-transition);
+}
+@media (prefers-reduced-motion: reduce) {
+ .offcanvas {
+ transition: none;
+ }
+}
+.offcanvas.offcanvas-start {
+ top: 0;
+ left: 0;
+ width: var(--bs-offcanvas-width);
+ border-right: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(-100%);
+}
+.offcanvas.offcanvas-end {
+ top: 0;
+ right: 0;
+ width: var(--bs-offcanvas-width);
+ border-left: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateX(100%);
+}
+.offcanvas.offcanvas-top {
+ top: 0;
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-bottom: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(-100%);
+}
+.offcanvas.offcanvas-bottom {
+ right: 0;
+ left: 0;
+ height: var(--bs-offcanvas-height);
+ max-height: 100%;
+ border-top: var(--bs-offcanvas-border-width) solid
+ var(--bs-offcanvas-border-color);
+ transform: translateY(100%);
+}
+.offcanvas.show:not(.hiding),
+.offcanvas.showing {
+ transform: none;
+}
+.offcanvas.hiding,
+.offcanvas.show,
+.offcanvas.showing {
+ visibility: visible;
+}
+.offcanvas-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1040;
+ width: 100vw;
+ height: 100vh;
+ background-color: #000;
+}
+.offcanvas-backdrop.fade {
+ opacity: 0;
+}
+.offcanvas-backdrop.show {
+ opacity: 0.5;
+}
+.offcanvas-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--bs-offcanvas-padding-y)
+ var(--bs-offcanvas-padding-x);
+}
+.offcanvas-header .btn-close {
+ padding: calc(var(--bs-offcanvas-padding-y) * 0.5)
+ calc(var(--bs-offcanvas-padding-x) * 0.5);
+ margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y));
+ margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x));
+ margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y));
+}
+.offcanvas-title {
+ margin-bottom: 0;
+ line-height: var(--bs-offcanvas-title-line-height);
+}
+.offcanvas-body {
+ flex-grow: 1;
+ padding: var(--bs-offcanvas-padding-y)
+ var(--bs-offcanvas-padding-x);
+ overflow-y: auto;
+}
+.placeholder {
+ display: inline-block;
+ min-height: 1em;
+ vertical-align: middle;
+ cursor: wait;
+ background-color: currentcolor;
+ opacity: 0.5;
+}
+.placeholder.btn::before {
+ display: inline-block;
+ content: "";
+}
+.placeholder-xs {
+ min-height: 0.6em;
+}
+.placeholder-sm {
+ min-height: 0.8em;
+}
+.placeholder-lg {
+ min-height: 1.2em;
+}
+.placeholder-glow .placeholder {
+ animation: placeholder-glow 2s ease-in-out infinite;
+}
+@keyframes placeholder-glow {
+ 50% {
+ opacity: 0.2;
+ }
+}
+.placeholder-wave {
+ -webkit-mask-image: linear-gradient(
+ 130deg,
+ #000 55%,
+ rgba(0, 0, 0, 0.8) 75%,
+ #000 95%
+ );
+ mask-image: linear-gradient(
+ 130deg,
+ #000 55%,
+ rgba(0, 0, 0, 0.8) 75%,
+ #000 95%
+ );
+ -webkit-mask-size: 200% 100%;
+ mask-size: 200% 100%;
+ animation: placeholder-wave 2s linear infinite;
+}
+@keyframes placeholder-wave {
+ 100% {
+ -webkit-mask-position: -200% 0%;
+ mask-position: -200% 0%;
+ }
+}
+.clearfix::after {
+ display: block;
+ clear: both;
+ content: "";
+}
+.text-bg-primary {
+ color: #fff !important;
+ background-color: RGBA(
+ var(--bs-primary-rgb),
+ var(--bs-bg-opacity, 1)
+ ) !important;
+}
+.text-bg-secondary {
+ color: #fff !important;
+ background-color: RGBA(
+ var(--bs-secondary-rgb),
+ var(--bs-bg-opacity, 1)
+ ) !important;
+}
+.text-bg-success {
+ color: #fff !important;
+ background-color: RGBA(
+ var(--bs-success-rgb),
+ var(--bs-bg-opacity, 1)
+ ) !important;
+}
+.text-bg-info {
+ color: #000 !important;
+ background-color: RGBA(
+ var(--bs-info-rgb),
+ var(--bs-bg-opacity, 1)
+ ) !important;
+}
+.text-bg-warning {
+ color: #000 !important;
+ background-color: RGBA(
+ var(--bs-warning-rgb),
+ var(--bs-bg-opacity, 1)
+ ) !important;
+}
+.text-bg-danger {
+ color: #fff !important;
+ background-color: RGBA(
+ var(--bs-danger-rgb),
+ var(--bs-bg-opacity, 1)
+ ) !important;
+}
+.text-bg-light {
+ color: #000 !important;
+ background-color: RGBA(
+ var(--bs-light-rgb),
+ var(--bs-bg-opacity, 1)
+ ) !important;
+}
+.text-bg-dark {
+ color: #fff !important;
+ background-color: RGBA(
+ var(--bs-dark-rgb),
+ var(--bs-bg-opacity, 1)
+ ) !important;
+}
+.link-primary {
+ color: RGBA(
+ var(--bs-primary-rgb),
+ var(--bs-link-opacity, 1)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ var(--bs-primary-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ var(--bs-primary-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-primary:focus,
+.link-primary:hover {
+ color: RGBA(10, 88, 202, var(--bs-link-opacity, 1)) !important;
+ -webkit-text-decoration-color: RGBA(
+ 10,
+ 88,
+ 202,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ 10,
+ 88,
+ 202,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-secondary {
+ color: RGBA(
+ var(--bs-secondary-rgb),
+ var(--bs-link-opacity, 1)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ var(--bs-secondary-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ var(--bs-secondary-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-secondary:focus,
+.link-secondary:hover {
+ color: RGBA(86, 94, 100, var(--bs-link-opacity, 1)) !important;
+ -webkit-text-decoration-color: RGBA(
+ 86,
+ 94,
+ 100,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ 86,
+ 94,
+ 100,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-success {
+ color: RGBA(
+ var(--bs-success-rgb),
+ var(--bs-link-opacity, 1)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ var(--bs-success-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ var(--bs-success-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-success:focus,
+.link-success:hover {
+ color: RGBA(20, 108, 67, var(--bs-link-opacity, 1)) !important;
+ -webkit-text-decoration-color: RGBA(
+ 20,
+ 108,
+ 67,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ 20,
+ 108,
+ 67,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-info {
+ color: RGBA(
+ var(--bs-info-rgb),
+ var(--bs-link-opacity, 1)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ var(--bs-info-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ var(--bs-info-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-info:focus,
+.link-info:hover {
+ color: RGBA(61, 213, 243, var(--bs-link-opacity, 1)) !important;
+ -webkit-text-decoration-color: RGBA(
+ 61,
+ 213,
+ 243,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ 61,
+ 213,
+ 243,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-warning {
+ color: RGBA(
+ var(--bs-warning-rgb),
+ var(--bs-link-opacity, 1)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ var(--bs-warning-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ var(--bs-warning-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-warning:focus,
+.link-warning:hover {
+ color: RGBA(255, 205, 57, var(--bs-link-opacity, 1)) !important;
+ -webkit-text-decoration-color: RGBA(
+ 255,
+ 205,
+ 57,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ 255,
+ 205,
+ 57,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-danger {
+ color: RGBA(
+ var(--bs-danger-rgb),
+ var(--bs-link-opacity, 1)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ var(--bs-danger-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ var(--bs-danger-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-danger:focus,
+.link-danger:hover {
+ color: RGBA(176, 42, 55, var(--bs-link-opacity, 1)) !important;
+ -webkit-text-decoration-color: RGBA(
+ 176,
+ 42,
+ 55,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ 176,
+ 42,
+ 55,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-light {
+ color: RGBA(
+ var(--bs-light-rgb),
+ var(--bs-link-opacity, 1)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ var(--bs-light-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ var(--bs-light-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-light:focus,
+.link-light:hover {
+ color: RGBA(
+ 249,
+ 250,
+ 251,
+ var(--bs-link-opacity, 1)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ 249,
+ 250,
+ 251,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ 249,
+ 250,
+ 251,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-dark {
+ color: RGBA(
+ var(--bs-dark-rgb),
+ var(--bs-link-opacity, 1)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ var(--bs-dark-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ var(--bs-dark-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-dark:focus,
+.link-dark:hover {
+ color: RGBA(26, 30, 33, var(--bs-link-opacity, 1)) !important;
+ -webkit-text-decoration-color: RGBA(
+ 26,
+ 30,
+ 33,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ 26,
+ 30,
+ 33,
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-body-emphasis {
+ color: RGBA(
+ var(--bs-emphasis-color-rgb),
+ var(--bs-link-opacity, 1)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ var(--bs-emphasis-color-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: RGBA(
+ var(--bs-emphasis-color-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-body-emphasis:focus,
+.link-body-emphasis:hover {
+ color: RGBA(
+ var(--bs-emphasis-color-rgb),
+ var(--bs-link-opacity, 0.75)
+ ) !important;
+ -webkit-text-decoration-color: RGBA(
+ var(--bs-emphasis-color-rgb),
+ var(--bs-link-underline-opacity, 0.75)
+ ) !important;
+ text-decoration-color: RGBA(
+ var(--bs-emphasis-color-rgb),
+ var(--bs-link-underline-opacity, 0.75)
+ ) !important;
+}
+.focus-ring:focus {
+ outline: 0;
+ box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0)
+ var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width)
+ var(--bs-focus-ring-color);
+}
+.icon-link {
+ display: inline-flex;
+ gap: 0.375rem;
+ align-items: center;
+ -webkit-text-decoration-color: rgba(
+ var(--bs-link-color-rgb),
+ var(--bs-link-opacity, 0.5)
+ );
+ text-decoration-color: rgba(
+ var(--bs-link-color-rgb),
+ var(--bs-link-opacity, 0.5)
+ );
+ text-underline-offset: 0.25em;
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+}
+.icon-link > .bi {
+ flex-shrink: 0;
+ width: 1em;
+ height: 1em;
+ fill: currentcolor;
+ transition: 0.2s ease-in-out transform;
+}
+@media (prefers-reduced-motion: reduce) {
+ .icon-link > .bi {
+ transition: none;
+ }
+}
+.icon-link-hover:focus-visible > .bi,
+.icon-link-hover:hover > .bi {
+ transform: var(
+ --bs-icon-link-transform,
+ translate3d(0.25em, 0, 0)
+ );
+}
+.ratio {
+ position: relative;
+ width: 100%;
+}
+.ratio::before {
+ display: block;
+ padding-top: var(--bs-aspect-ratio);
+ content: "";
+}
+.ratio > * {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+.ratio-1x1 {
+ --bs-aspect-ratio: 100%;
+}
+.ratio-4x3 {
+ --bs-aspect-ratio: 75%;
+}
+.ratio-16x9 {
+ --bs-aspect-ratio: 56.25%;
+}
+.ratio-21x9 {
+ --bs-aspect-ratio: 42.8571428571%;
+}
+.fixed-top {
+ position: fixed;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 1030;
+}
+.fixed-bottom {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1030;
+}
+.sticky-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+}
+.sticky-bottom {
+ position: -webkit-sticky;
+ position: sticky;
+ bottom: 0;
+ z-index: 1020;
+}
+@media (min-width: 576px) {
+ .sticky-sm-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+ .sticky-sm-bottom {
+ position: -webkit-sticky;
+ position: sticky;
+ bottom: 0;
+ z-index: 1020;
+ }
+}
+@media (min-width: 768px) {
+ .sticky-md-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+ .sticky-md-bottom {
+ position: -webkit-sticky;
+ position: sticky;
+ bottom: 0;
+ z-index: 1020;
+ }
+}
+@media (min-width: 992px) {
+ .sticky-lg-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+ .sticky-lg-bottom {
+ position: -webkit-sticky;
+ position: sticky;
+ bottom: 0;
+ z-index: 1020;
+ }
+}
+@media (min-width: 1200px) {
+ .sticky-xl-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+ .sticky-xl-bottom {
+ position: -webkit-sticky;
+ position: sticky;
+ bottom: 0;
+ z-index: 1020;
+ }
+}
+@media (min-width: 1400px) {
+ .sticky-xxl-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+ .sticky-xxl-bottom {
+ position: -webkit-sticky;
+ position: sticky;
+ bottom: 0;
+ z-index: 1020;
+ }
+}
+.hstack {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ align-self: stretch;
+}
+.vstack {
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ align-self: stretch;
+}
+.visually-hidden,
+.visually-hidden-focusable:not(:focus):not(:focus-within) {
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ margin: -1px !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+}
+.visually-hidden-focusable:not(:focus):not(:focus-within):not(
+ caption
+ ),
+.visually-hidden:not(caption) {
+ position: absolute !important;
+}
+.stretched-link::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ content: "";
+}
+.text-truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.vr {
+ display: inline-block;
+ align-self: stretch;
+ width: var(--bs-border-width);
+ min-height: 1em;
+ background-color: currentcolor;
+ opacity: 0.25;
+}
+.align-baseline {
+ vertical-align: baseline !important;
+}
+.align-top {
+ vertical-align: top !important;
+}
+.align-middle {
+ vertical-align: middle !important;
+}
+.align-bottom {
+ vertical-align: bottom !important;
+}
+.align-text-bottom {
+ vertical-align: text-bottom !important;
+}
+.align-text-top {
+ vertical-align: text-top !important;
+}
+.float-start {
+ float: left !important;
+}
+.float-end {
+ float: right !important;
+}
+.float-none {
+ float: none !important;
+}
+.object-fit-contain {
+ -o-object-fit: contain !important;
+ object-fit: contain !important;
+}
+.object-fit-cover {
+ -o-object-fit: cover !important;
+ object-fit: cover !important;
+}
+.object-fit-fill {
+ -o-object-fit: fill !important;
+ object-fit: fill !important;
+}
+.object-fit-scale {
+ -o-object-fit: scale-down !important;
+ object-fit: scale-down !important;
+}
+.object-fit-none {
+ -o-object-fit: none !important;
+ object-fit: none !important;
+}
+.opacity-0 {
+ opacity: 0 !important;
+}
+.opacity-25 {
+ opacity: 0.25 !important;
+}
+.opacity-50 {
+ opacity: 0.5 !important;
+}
+.opacity-75 {
+ opacity: 0.75 !important;
+}
+.opacity-100 {
+ opacity: 1 !important;
+}
+.overflow-auto {
+ overflow: auto !important;
+}
+.overflow-hidden {
+ overflow: hidden !important;
+}
+.overflow-visible {
+ overflow: visible !important;
+}
+.overflow-scroll {
+ overflow: scroll !important;
+}
+.overflow-x-auto {
+ overflow-x: auto !important;
+}
+.overflow-x-hidden {
+ overflow-x: hidden !important;
+}
+.overflow-x-visible {
+ overflow-x: visible !important;
+}
+.overflow-x-scroll {
+ overflow-x: scroll !important;
+}
+.overflow-y-auto {
+ overflow-y: auto !important;
+}
+.overflow-y-hidden {
+ overflow-y: hidden !important;
+}
+.overflow-y-visible {
+ overflow-y: visible !important;
+}
+.overflow-y-scroll {
+ overflow-y: scroll !important;
+}
+.d-inline {
+ display: inline !important;
+}
+.d-inline-block {
+ display: inline-block !important;
+}
+.d-block {
+ display: block !important;
+}
+.d-grid {
+ display: grid !important;
+}
+.d-inline-grid {
+ display: inline-grid !important;
+}
+.d-table {
+ display: table !important;
+}
+.d-table-row {
+ display: table-row !important;
+}
+.d-table-cell {
+ display: table-cell !important;
+}
+.d-flex {
+ display: flex !important;
+}
+.d-inline-flex {
+ display: inline-flex !important;
+}
+.d-none {
+ display: none !important;
+}
+.shadow {
+ box-shadow: var(--bs-box-shadow) !important;
+}
+.shadow-sm {
+ box-shadow: var(--bs-box-shadow-sm) !important;
+}
+.shadow-lg {
+ box-shadow: var(--bs-box-shadow-lg) !important;
+}
+.shadow-none {
+ box-shadow: none !important;
+}
+.focus-ring-primary {
+ --bs-focus-ring-color: rgba(
+ var(--bs-primary-rgb),
+ var(--bs-focus-ring-opacity)
+ );
+}
+.focus-ring-secondary {
+ --bs-focus-ring-color: rgba(
+ var(--bs-secondary-rgb),
+ var(--bs-focus-ring-opacity)
+ );
+}
+.focus-ring-success {
+ --bs-focus-ring-color: rgba(
+ var(--bs-success-rgb),
+ var(--bs-focus-ring-opacity)
+ );
+}
+.focus-ring-info {
+ --bs-focus-ring-color: rgba(
+ var(--bs-info-rgb),
+ var(--bs-focus-ring-opacity)
+ );
+}
+.focus-ring-warning {
+ --bs-focus-ring-color: rgba(
+ var(--bs-warning-rgb),
+ var(--bs-focus-ring-opacity)
+ );
+}
+.focus-ring-danger {
+ --bs-focus-ring-color: rgba(
+ var(--bs-danger-rgb),
+ var(--bs-focus-ring-opacity)
+ );
+}
+.focus-ring-light {
+ --bs-focus-ring-color: rgba(
+ var(--bs-light-rgb),
+ var(--bs-focus-ring-opacity)
+ );
+}
+.focus-ring-dark {
+ --bs-focus-ring-color: rgba(
+ var(--bs-dark-rgb),
+ var(--bs-focus-ring-opacity)
+ );
+}
+.position-static {
+ position: static !important;
+}
+.position-relative {
+ position: relative !important;
+}
+.position-absolute {
+ position: absolute !important;
+}
+.position-fixed {
+ position: fixed !important;
+}
+.position-sticky {
+ position: -webkit-sticky !important;
+ position: sticky !important;
+}
+.top-0 {
+ top: 0 !important;
+}
+.top-50 {
+ top: 50% !important;
+}
+.top-100 {
+ top: 100% !important;
+}
+.bottom-0 {
+ bottom: 0 !important;
+}
+.bottom-50 {
+ bottom: 50% !important;
+}
+.bottom-100 {
+ bottom: 100% !important;
+}
+.start-0 {
+ left: 0 !important;
+}
+.start-50 {
+ left: 50% !important;
+}
+.start-100 {
+ left: 100% !important;
+}
+.end-0 {
+ right: 0 !important;
+}
+.end-50 {
+ right: 50% !important;
+}
+.end-100 {
+ right: 100% !important;
+}
+.translate-middle {
+ transform: translate(-50%, -50%) !important;
+}
+.translate-middle-x {
+ transform: translateX(-50%) !important;
+}
+.translate-middle-y {
+ transform: translateY(-50%) !important;
+}
+.border {
+ border: var(--bs-border-width) var(--bs-border-style)
+ var(--bs-border-color) !important;
+}
+.border-0 {
+ border: 0 !important;
+}
+.border-top {
+ border-top: var(--bs-border-width) var(--bs-border-style)
+ var(--bs-border-color) !important;
+}
+.border-top-0 {
+ border-top: 0 !important;
+}
+.border-end {
+ border-right: var(--bs-border-width) var(--bs-border-style)
+ var(--bs-border-color) !important;
+}
+.border-end-0 {
+ border-right: 0 !important;
+}
+.border-bottom {
+ border-bottom: var(--bs-border-width) var(--bs-border-style)
+ var(--bs-border-color) !important;
+}
+.border-bottom-0 {
+ border-bottom: 0 !important;
+}
+.border-start {
+ border-left: var(--bs-border-width) var(--bs-border-style)
+ var(--bs-border-color) !important;
+}
+.border-start-0 {
+ border-left: 0 !important;
+}
+.border-primary {
+ --bs-border-opacity: 1;
+ border-color: rgba(
+ var(--bs-primary-rgb),
+ var(--bs-border-opacity)
+ ) !important;
+}
+.border-secondary {
+ --bs-border-opacity: 1;
+ border-color: rgba(
+ var(--bs-secondary-rgb),
+ var(--bs-border-opacity)
+ ) !important;
+}
+.border-success {
+ --bs-border-opacity: 1;
+ border-color: rgba(
+ var(--bs-success-rgb),
+ var(--bs-border-opacity)
+ ) !important;
+}
+.border-info {
+ --bs-border-opacity: 1;
+ border-color: rgba(
+ var(--bs-info-rgb),
+ var(--bs-border-opacity)
+ ) !important;
+}
+.border-warning {
+ --bs-border-opacity: 1;
+ border-color: rgba(
+ var(--bs-warning-rgb),
+ var(--bs-border-opacity)
+ ) !important;
+}
+.border-danger {
+ --bs-border-opacity: 1;
+ border-color: rgba(
+ var(--bs-danger-rgb),
+ var(--bs-border-opacity)
+ ) !important;
+}
+.border-light {
+ --bs-border-opacity: 1;
+ border-color: rgba(
+ var(--bs-light-rgb),
+ var(--bs-border-opacity)
+ ) !important;
+}
+.border-dark {
+ --bs-border-opacity: 1;
+ border-color: rgba(
+ var(--bs-dark-rgb),
+ var(--bs-border-opacity)
+ ) !important;
+}
+.border-black {
+ --bs-border-opacity: 1;
+ border-color: rgba(
+ var(--bs-black-rgb),
+ var(--bs-border-opacity)
+ ) !important;
+}
+.border-white {
+ --bs-border-opacity: 1;
+ border-color: rgba(
+ var(--bs-white-rgb),
+ var(--bs-border-opacity)
+ ) !important;
+}
+.border-primary-subtle {
+ border-color: var(--bs-primary-border-subtle) !important;
+}
+.border-secondary-subtle {
+ border-color: var(--bs-secondary-border-subtle) !important;
+}
+.border-success-subtle {
+ border-color: var(--bs-success-border-subtle) !important;
+}
+.border-info-subtle {
+ border-color: var(--bs-info-border-subtle) !important;
+}
+.border-warning-subtle {
+ border-color: var(--bs-warning-border-subtle) !important;
+}
+.border-danger-subtle {
+ border-color: var(--bs-danger-border-subtle) !important;
+}
+.border-light-subtle {
+ border-color: var(--bs-light-border-subtle) !important;
+}
+.border-dark-subtle {
+ border-color: var(--bs-dark-border-subtle) !important;
+}
+.border-1 {
+ border-width: 1px !important;
+}
+.border-2 {
+ border-width: 2px !important;
+}
+.border-3 {
+ border-width: 3px !important;
+}
+.border-4 {
+ border-width: 4px !important;
+}
+.border-5 {
+ border-width: 5px !important;
+}
+.border-opacity-10 {
+ --bs-border-opacity: 0.1;
+}
+.border-opacity-25 {
+ --bs-border-opacity: 0.25;
+}
+.border-opacity-50 {
+ --bs-border-opacity: 0.5;
+}
+.border-opacity-75 {
+ --bs-border-opacity: 0.75;
+}
+.border-opacity-100 {
+ --bs-border-opacity: 1;
+}
+.w-25 {
+ width: 25% !important;
+}
+.w-50 {
+ width: 50% !important;
+}
+.w-75 {
+ width: 75% !important;
+}
+.w-100 {
+ width: 100% !important;
+}
+.w-auto {
+ width: auto !important;
+}
+.mw-100 {
+ max-width: 100% !important;
+}
+.vw-100 {
+ width: 100vw !important;
+}
+.min-vw-100 {
+ min-width: 100vw !important;
+}
+.h-25 {
+ height: 25% !important;
+}
+.h-50 {
+ height: 50% !important;
+}
+.h-75 {
+ height: 75% !important;
+}
+.h-100 {
+ height: 100% !important;
+}
+.h-auto {
+ height: auto !important;
+}
+.mh-100 {
+ max-height: 100% !important;
+}
+.vh-100 {
+ height: 100vh !important;
+}
+.min-vh-100 {
+ min-height: 100vh !important;
+}
+.flex-fill {
+ flex: 1 1 auto !important;
+}
+.flex-row {
+ flex-direction: row !important;
+}
+.flex-column {
+ flex-direction: column !important;
+}
+.flex-row-reverse {
+ flex-direction: row-reverse !important;
+}
+.flex-column-reverse {
+ flex-direction: column-reverse !important;
+}
+.flex-grow-0 {
+ flex-grow: 0 !important;
+}
+.flex-grow-1 {
+ flex-grow: 1 !important;
+}
+.flex-shrink-0 {
+ flex-shrink: 0 !important;
+}
+.flex-shrink-1 {
+ flex-shrink: 1 !important;
+}
+.flex-wrap {
+ flex-wrap: wrap !important;
+}
+.flex-nowrap {
+ flex-wrap: nowrap !important;
+}
+.flex-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+}
+.justify-content-start {
+ justify-content: flex-start !important;
+}
+.justify-content-end {
+ justify-content: flex-end !important;
+}
+.justify-content-center {
+ justify-content: center !important;
+}
+.justify-content-between {
+ justify-content: space-between !important;
+}
+.justify-content-around {
+ justify-content: space-around !important;
+}
+.justify-content-evenly {
+ justify-content: space-evenly !important;
+}
+.align-items-start {
+ align-items: flex-start !important;
+}
+.align-items-end {
+ align-items: flex-end !important;
+}
+.align-items-center {
+ align-items: center !important;
+}
+.align-items-baseline {
+ align-items: baseline !important;
+}
+.align-items-stretch {
+ align-items: stretch !important;
+}
+.align-content-start {
+ align-content: flex-start !important;
+}
+.align-content-end {
+ align-content: flex-end !important;
+}
+.align-content-center {
+ align-content: center !important;
+}
+.align-content-between {
+ align-content: space-between !important;
+}
+.align-content-around {
+ align-content: space-around !important;
+}
+.align-content-stretch {
+ align-content: stretch !important;
+}
+.align-self-auto {
+ align-self: auto !important;
+}
+.align-self-start {
+ align-self: flex-start !important;
+}
+.align-self-end {
+ align-self: flex-end !important;
+}
+.align-self-center {
+ align-self: center !important;
+}
+.align-self-baseline {
+ align-self: baseline !important;
+}
+.align-self-stretch {
+ align-self: stretch !important;
+}
+.order-first {
+ order: -1 !important;
+}
+.order-0 {
+ order: 0 !important;
+}
+.order-1 {
+ order: 1 !important;
+}
+.order-2 {
+ order: 2 !important;
+}
+.order-3 {
+ order: 3 !important;
+}
+.order-4 {
+ order: 4 !important;
+}
+.order-5 {
+ order: 5 !important;
+}
+.order-last {
+ order: 6 !important;
+}
+.m-0 {
+ margin: 0 !important;
+}
+.m-1 {
+ margin: 0.25rem !important;
+}
+.m-2 {
+ margin: 0.5rem !important;
+}
+.m-3 {
+ margin: 1rem !important;
+}
+.m-4 {
+ margin: 1.5rem !important;
+}
+.m-5 {
+ margin: 3rem !important;
+}
+.m-auto {
+ margin: auto !important;
+}
+.mx-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+}
+.mx-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+}
+.mx-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+}
+.mx-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+}
+.mx-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+}
+.mx-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+}
+.mx-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+}
+.my-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+}
+.my-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+}
+.my-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+}
+.my-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+}
+.my-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+}
+.my-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+}
+.my-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+}
+.mt-0 {
+ margin-top: 0 !important;
+}
+.mt-1 {
+ margin-top: 0.25rem !important;
+}
+.mt-2 {
+ margin-top: 0.5rem !important;
+}
+.mt-3 {
+ margin-top: 1rem !important;
+}
+.mt-4 {
+ margin-top: 1.5rem !important;
+}
+.mt-5 {
+ margin-top: 3rem !important;
+}
+.mt-auto {
+ margin-top: auto !important;
+}
+.me-0 {
+ margin-right: 0 !important;
+}
+.me-1 {
+ margin-right: 0.25rem !important;
+}
+.me-2 {
+ margin-right: 0.5rem !important;
+}
+.me-3 {
+ margin-right: 1rem !important;
+}
+.me-4 {
+ margin-right: 1.5rem !important;
+}
+.me-5 {
+ margin-right: 3rem !important;
+}
+.me-auto {
+ margin-right: auto !important;
+}
+.mb-0 {
+ margin-bottom: 0 !important;
+}
+.mb-1 {
+ margin-bottom: 0.25rem !important;
+}
+.mb-2 {
+ margin-bottom: 0.5rem !important;
+}
+.mb-3 {
+ margin-bottom: 1rem !important;
+}
+.mb-4 {
+ margin-bottom: 1.5rem !important;
+}
+.mb-5 {
+ margin-bottom: 3rem !important;
+}
+.mb-auto {
+ margin-bottom: auto !important;
+}
+.ms-0 {
+ margin-left: 0 !important;
+}
+.ms-1 {
+ margin-left: 0.25rem !important;
+}
+.ms-2 {
+ margin-left: 0.5rem !important;
+}
+.ms-3 {
+ margin-left: 1rem !important;
+}
+.ms-4 {
+ margin-left: 1.5rem !important;
+}
+.ms-5 {
+ margin-left: 3rem !important;
+}
+.ms-auto {
+ margin-left: auto !important;
+}
+.p-0 {
+ padding: 0 !important;
+}
+.p-1 {
+ padding: 0.25rem !important;
+}
+.p-2 {
+ padding: 0.5rem !important;
+}
+.p-3 {
+ padding: 1rem !important;
+}
+.p-4 {
+ padding: 1.5rem !important;
+}
+.p-5 {
+ padding: 3rem !important;
+}
+.px-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+}
+.px-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+}
+.px-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+}
+.px-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+}
+.px-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+}
+.px-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+}
+.py-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+}
+.py-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+}
+.py-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+}
+.py-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+}
+.py-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+}
+.py-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+}
+.pt-0 {
+ padding-top: 0 !important;
+}
+.pt-1 {
+ padding-top: 0.25rem !important;
+}
+.pt-2 {
+ padding-top: 0.5rem !important;
+}
+.pt-3 {
+ padding-top: 1rem !important;
+}
+.pt-4 {
+ padding-top: 1.5rem !important;
+}
+.pt-5 {
+ padding-top: 3rem !important;
+}
+.pe-0 {
+ padding-right: 0 !important;
+}
+.pe-1 {
+ padding-right: 0.25rem !important;
+}
+.pe-2 {
+ padding-right: 0.5rem !important;
+}
+.pe-3 {
+ padding-right: 1rem !important;
+}
+.pe-4 {
+ padding-right: 1.5rem !important;
+}
+.pe-5 {
+ padding-right: 3rem !important;
+}
+.pb-0 {
+ padding-bottom: 0 !important;
+}
+.pb-1 {
+ padding-bottom: 0.25rem !important;
+}
+.pb-2 {
+ padding-bottom: 0.5rem !important;
+}
+.pb-3 {
+ padding-bottom: 1rem !important;
+}
+.pb-4 {
+ padding-bottom: 1.5rem !important;
+}
+.pb-5 {
+ padding-bottom: 3rem !important;
+}
+.ps-0 {
+ padding-left: 0 !important;
+}
+.ps-1 {
+ padding-left: 0.25rem !important;
+}
+.ps-2 {
+ padding-left: 0.5rem !important;
+}
+.ps-3 {
+ padding-left: 1rem !important;
+}
+.ps-4 {
+ padding-left: 1.5rem !important;
+}
+.ps-5 {
+ padding-left: 3rem !important;
+}
+.gap-0 {
+ gap: 0 !important;
+}
+.gap-1 {
+ gap: 0.25rem !important;
+}
+.gap-2 {
+ gap: 0.5rem !important;
+}
+.gap-3 {
+ gap: 1rem !important;
+}
+.gap-4 {
+ gap: 1.5rem !important;
+}
+.gap-5 {
+ gap: 3rem !important;
+}
+.row-gap-0 {
+ row-gap: 0 !important;
+}
+.row-gap-1 {
+ row-gap: 0.25rem !important;
+}
+.row-gap-2 {
+ row-gap: 0.5rem !important;
+}
+.row-gap-3 {
+ row-gap: 1rem !important;
+}
+.row-gap-4 {
+ row-gap: 1.5rem !important;
+}
+.row-gap-5 {
+ row-gap: 3rem !important;
+}
+.column-gap-0 {
+ -moz-column-gap: 0 !important;
+ column-gap: 0 !important;
+}
+.column-gap-1 {
+ -moz-column-gap: 0.25rem !important;
+ column-gap: 0.25rem !important;
+}
+.column-gap-2 {
+ -moz-column-gap: 0.5rem !important;
+ column-gap: 0.5rem !important;
+}
+.column-gap-3 {
+ -moz-column-gap: 1rem !important;
+ column-gap: 1rem !important;
+}
+.column-gap-4 {
+ -moz-column-gap: 1.5rem !important;
+ column-gap: 1.5rem !important;
+}
+.column-gap-5 {
+ -moz-column-gap: 3rem !important;
+ column-gap: 3rem !important;
+}
+.font-monospace {
+ font-family: var(--bs-font-monospace) !important;
+}
+.fs-1 {
+ font-size: calc(1.375rem + 1.5vw) !important;
+}
+.fs-2 {
+ font-size: calc(1.325rem + 0.9vw) !important;
+}
+.fs-3 {
+ font-size: calc(1.3rem + 0.6vw) !important;
+}
+.fs-4 {
+ font-size: calc(1.275rem + 0.3vw) !important;
+}
+.fs-5 {
+ font-size: 1.25rem !important;
+}
+.fs-6 {
+ font-size: 1rem !important;
+}
+.fst-italic {
+ font-style: italic !important;
+}
+.fst-normal {
+ font-style: normal !important;
+}
+.fw-lighter {
+ font-weight: lighter !important;
+}
+.fw-light {
+ font-weight: 300 !important;
+}
+.fw-normal {
+ font-weight: 400 !important;
+}
+.fw-medium {
+ font-weight: 500 !important;
+}
+.fw-semibold {
+ font-weight: 600 !important;
+}
+.fw-bold {
+ font-weight: 700 !important;
+}
+.fw-bolder {
+ font-weight: bolder !important;
+}
+.lh-1 {
+ line-height: 1 !important;
+}
+.lh-sm {
+ line-height: 1.25 !important;
+}
+.lh-base {
+ line-height: 1.5 !important;
+}
+.lh-lg {
+ line-height: 2 !important;
+}
+.text-start {
+ text-align: left !important;
+}
+.text-end {
+ text-align: right !important;
+}
+.text-center {
+ text-align: center !important;
+}
+.text-decoration-none {
+ text-decoration: none !important;
+}
+.text-decoration-underline {
+ text-decoration: underline !important;
+}
+.text-decoration-line-through {
+ text-decoration: line-through !important;
+}
+.text-lowercase {
+ text-transform: lowercase !important;
+}
+.text-uppercase {
+ text-transform: uppercase !important;
+}
+.text-capitalize {
+ text-transform: capitalize !important;
+}
+.text-wrap {
+ white-space: normal !important;
+}
+.text-nowrap {
+ white-space: nowrap !important;
+}
+.text-break {
+ word-wrap: break-word !important;
+ word-break: break-word !important;
+}
+.text-primary {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-primary-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-secondary {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-secondary-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-success {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-success-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-info {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-info-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-warning {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-warning-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-danger {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-danger-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-light {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-light-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-dark {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-dark-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-black {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-black-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-white {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-white-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-body {
+ --bs-text-opacity: 1;
+ color: rgba(
+ var(--bs-body-color-rgb),
+ var(--bs-text-opacity)
+ ) !important;
+}
+.text-muted {
+ --bs-text-opacity: 1;
+ color: var(--bs-secondary-color) !important;
+}
+.text-black-50 {
+ --bs-text-opacity: 1;
+ color: rgba(0, 0, 0, 0.5) !important;
+}
+.text-white-50 {
+ --bs-text-opacity: 1;
+ color: rgba(255, 255, 255, 0.5) !important;
+}
+.text-body-secondary {
+ --bs-text-opacity: 1;
+ color: var(--bs-secondary-color) !important;
+}
+.text-body-tertiary {
+ --bs-text-opacity: 1;
+ color: var(--bs-tertiary-color) !important;
+}
+.text-body-emphasis {
+ --bs-text-opacity: 1;
+ color: var(--bs-emphasis-color) !important;
+}
+.text-reset {
+ --bs-text-opacity: 1;
+ color: inherit !important;
+}
+.text-opacity-25 {
+ --bs-text-opacity: 0.25;
+}
+.text-opacity-50 {
+ --bs-text-opacity: 0.5;
+}
+.text-opacity-75 {
+ --bs-text-opacity: 0.75;
+}
+.text-opacity-100 {
+ --bs-text-opacity: 1;
+}
+.text-primary-emphasis {
+ color: var(--bs-primary-text-emphasis) !important;
+}
+.text-secondary-emphasis {
+ color: var(--bs-secondary-text-emphasis) !important;
+}
+.text-success-emphasis {
+ color: var(--bs-success-text-emphasis) !important;
+}
+.text-info-emphasis {
+ color: var(--bs-info-text-emphasis) !important;
+}
+.text-warning-emphasis {
+ color: var(--bs-warning-text-emphasis) !important;
+}
+.text-danger-emphasis {
+ color: var(--bs-danger-text-emphasis) !important;
+}
+.text-light-emphasis {
+ color: var(--bs-light-text-emphasis) !important;
+}
+.text-dark-emphasis {
+ color: var(--bs-dark-text-emphasis) !important;
+}
+.link-opacity-10 {
+ --bs-link-opacity: 0.1;
+}
+.link-opacity-10-hover:hover {
+ --bs-link-opacity: 0.1;
+}
+.link-opacity-25 {
+ --bs-link-opacity: 0.25;
+}
+.link-opacity-25-hover:hover {
+ --bs-link-opacity: 0.25;
+}
+.link-opacity-50 {
+ --bs-link-opacity: 0.5;
+}
+.link-opacity-50-hover:hover {
+ --bs-link-opacity: 0.5;
+}
+.link-opacity-75 {
+ --bs-link-opacity: 0.75;
+}
+.link-opacity-75-hover:hover {
+ --bs-link-opacity: 0.75;
+}
+.link-opacity-100 {
+ --bs-link-opacity: 1;
+}
+.link-opacity-100-hover:hover {
+ --bs-link-opacity: 1;
+}
+.link-offset-1 {
+ text-underline-offset: 0.125em !important;
+}
+.link-offset-1-hover:hover {
+ text-underline-offset: 0.125em !important;
+}
+.link-offset-2 {
+ text-underline-offset: 0.25em !important;
+}
+.link-offset-2-hover:hover {
+ text-underline-offset: 0.25em !important;
+}
+.link-offset-3 {
+ text-underline-offset: 0.375em !important;
+}
+.link-offset-3-hover:hover {
+ text-underline-offset: 0.375em !important;
+}
+.link-underline-primary {
+ --bs-link-underline-opacity: 1;
+ -webkit-text-decoration-color: rgba(
+ var(--bs-primary-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+ text-decoration-color: rgba(
+ var(--bs-primary-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+}
+.link-underline-secondary {
+ --bs-link-underline-opacity: 1;
+ -webkit-text-decoration-color: rgba(
+ var(--bs-secondary-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+ text-decoration-color: rgba(
+ var(--bs-secondary-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+}
+.link-underline-success {
+ --bs-link-underline-opacity: 1;
+ -webkit-text-decoration-color: rgba(
+ var(--bs-success-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+ text-decoration-color: rgba(
+ var(--bs-success-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+}
+.link-underline-info {
+ --bs-link-underline-opacity: 1;
+ -webkit-text-decoration-color: rgba(
+ var(--bs-info-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+ text-decoration-color: rgba(
+ var(--bs-info-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+}
+.link-underline-warning {
+ --bs-link-underline-opacity: 1;
+ -webkit-text-decoration-color: rgba(
+ var(--bs-warning-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+ text-decoration-color: rgba(
+ var(--bs-warning-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+}
+.link-underline-danger {
+ --bs-link-underline-opacity: 1;
+ -webkit-text-decoration-color: rgba(
+ var(--bs-danger-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+ text-decoration-color: rgba(
+ var(--bs-danger-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+}
+.link-underline-light {
+ --bs-link-underline-opacity: 1;
+ -webkit-text-decoration-color: rgba(
+ var(--bs-light-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+ text-decoration-color: rgba(
+ var(--bs-light-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+}
+.link-underline-dark {
+ --bs-link-underline-opacity: 1;
+ -webkit-text-decoration-color: rgba(
+ var(--bs-dark-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+ text-decoration-color: rgba(
+ var(--bs-dark-rgb),
+ var(--bs-link-underline-opacity)
+ ) !important;
+}
+.link-underline {
+ --bs-link-underline-opacity: 1;
+ -webkit-text-decoration-color: rgba(
+ var(--bs-link-color-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+ text-decoration-color: rgba(
+ var(--bs-link-color-rgb),
+ var(--bs-link-underline-opacity, 1)
+ ) !important;
+}
+.link-underline-opacity-0 {
+ --bs-link-underline-opacity: 0;
+}
+.link-underline-opacity-0-hover:hover {
+ --bs-link-underline-opacity: 0;
+}
+.link-underline-opacity-10 {
+ --bs-link-underline-opacity: 0.1;
+}
+.link-underline-opacity-10-hover:hover {
+ --bs-link-underline-opacity: 0.1;
+}
+.link-underline-opacity-25 {
+ --bs-link-underline-opacity: 0.25;
+}
+.link-underline-opacity-25-hover:hover {
+ --bs-link-underline-opacity: 0.25;
+}
+.link-underline-opacity-50 {
+ --bs-link-underline-opacity: 0.5;
+}
+.link-underline-opacity-50-hover:hover {
+ --bs-link-underline-opacity: 0.5;
+}
+.link-underline-opacity-75 {
+ --bs-link-underline-opacity: 0.75;
+}
+.link-underline-opacity-75-hover:hover {
+ --bs-link-underline-opacity: 0.75;
+}
+.link-underline-opacity-100 {
+ --bs-link-underline-opacity: 1;
+}
+.link-underline-opacity-100-hover:hover {
+ --bs-link-underline-opacity: 1;
+}
+.bg-primary {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-primary-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-secondary {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-secondary-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-success {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-success-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-info {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-info-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-warning {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-warning-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-danger {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-danger-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-light {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-light-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-dark {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-dark-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-black {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-black-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-white {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-white-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-body {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-body-bg-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-transparent {
+ --bs-bg-opacity: 1;
+ background-color: transparent !important;
+}
+.bg-body-secondary {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-secondary-bg-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-body-tertiary {
+ --bs-bg-opacity: 1;
+ background-color: rgba(
+ var(--bs-tertiary-bg-rgb),
+ var(--bs-bg-opacity)
+ ) !important;
+}
+.bg-opacity-10 {
+ --bs-bg-opacity: 0.1;
+}
+.bg-opacity-25 {
+ --bs-bg-opacity: 0.25;
+}
+.bg-opacity-50 {
+ --bs-bg-opacity: 0.5;
+}
+.bg-opacity-75 {
+ --bs-bg-opacity: 0.75;
+}
+.bg-opacity-100 {
+ --bs-bg-opacity: 1;
+}
+.bg-primary-subtle {
+ background-color: var(--bs-primary-bg-subtle) !important;
+}
+.bg-secondary-subtle {
+ background-color: var(--bs-secondary-bg-subtle) !important;
+}
+.bg-success-subtle {
+ background-color: var(--bs-success-bg-subtle) !important;
+}
+.bg-info-subtle {
+ background-color: var(--bs-info-bg-subtle) !important;
+}
+.bg-warning-subtle {
+ background-color: var(--bs-warning-bg-subtle) !important;
+}
+.bg-danger-subtle {
+ background-color: var(--bs-danger-bg-subtle) !important;
+}
+.bg-light-subtle {
+ background-color: var(--bs-light-bg-subtle) !important;
+}
+.bg-dark-subtle {
+ background-color: var(--bs-dark-bg-subtle) !important;
+}
+.bg-gradient {
+ background-image: var(--bs-gradient) !important;
+}
+.user-select-all {
+ -webkit-user-select: all !important;
+ -moz-user-select: all !important;
+ user-select: all !important;
+}
+.user-select-auto {
+ -webkit-user-select: auto !important;
+ -moz-user-select: auto !important;
+ user-select: auto !important;
+}
+.user-select-none {
+ -webkit-user-select: none !important;
+ -moz-user-select: none !important;
+ user-select: none !important;
+}
+.pe-none {
+ pointer-events: none !important;
+}
+.pe-auto {
+ pointer-events: auto !important;
+}
+.rounded {
+ border-radius: var(--bs-border-radius) !important;
+}
+.rounded-0 {
+ border-radius: 0 !important;
+}
+.rounded-1 {
+ border-radius: var(--bs-border-radius-sm) !important;
+}
+.rounded-2 {
+ border-radius: var(--bs-border-radius) !important;
+}
+.rounded-3 {
+ border-radius: var(--bs-border-radius-lg) !important;
+}
+.rounded-4 {
+ border-radius: var(--bs-border-radius-xl) !important;
+}
+.rounded-5 {
+ border-radius: var(--bs-border-radius-xxl) !important;
+}
+.rounded-circle {
+ border-radius: 50% !important;
+}
+.rounded-pill {
+ border-radius: var(--bs-border-radius-pill) !important;
+}
+.rounded-top {
+ border-top-left-radius: var(--bs-border-radius) !important;
+ border-top-right-radius: var(--bs-border-radius) !important;
+}
+.rounded-top-0 {
+ border-top-left-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+}
+.rounded-top-1 {
+ border-top-left-radius: var(--bs-border-radius-sm) !important;
+ border-top-right-radius: var(--bs-border-radius-sm) !important;
+}
+.rounded-top-2 {
+ border-top-left-radius: var(--bs-border-radius) !important;
+ border-top-right-radius: var(--bs-border-radius) !important;
+}
+.rounded-top-3 {
+ border-top-left-radius: var(--bs-border-radius-lg) !important;
+ border-top-right-radius: var(--bs-border-radius-lg) !important;
+}
+.rounded-top-4 {
+ border-top-left-radius: var(--bs-border-radius-xl) !important;
+ border-top-right-radius: var(--bs-border-radius-xl) !important;
+}
+.rounded-top-5 {
+ border-top-left-radius: var(--bs-border-radius-xxl) !important;
+ border-top-right-radius: var(--bs-border-radius-xxl) !important;
+}
+.rounded-top-circle {
+ border-top-left-radius: 50% !important;
+ border-top-right-radius: 50% !important;
+}
+.rounded-top-pill {
+ border-top-left-radius: var(--bs-border-radius-pill) !important;
+ border-top-right-radius: var(
+ --bs-border-radius-pill
+ ) !important;
+}
+.rounded-end {
+ border-top-right-radius: var(--bs-border-radius) !important;
+ border-bottom-right-radius: var(--bs-border-radius) !important;
+}
+.rounded-end-0 {
+ border-top-right-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+}
+.rounded-end-1 {
+ border-top-right-radius: var(--bs-border-radius-sm) !important;
+ border-bottom-right-radius: var(
+ --bs-border-radius-sm
+ ) !important;
+}
+.rounded-end-2 {
+ border-top-right-radius: var(--bs-border-radius) !important;
+ border-bottom-right-radius: var(--bs-border-radius) !important;
+}
+.rounded-end-3 {
+ border-top-right-radius: var(--bs-border-radius-lg) !important;
+ border-bottom-right-radius: var(
+ --bs-border-radius-lg
+ ) !important;
+}
+.rounded-end-4 {
+ border-top-right-radius: var(--bs-border-radius-xl) !important;
+ border-bottom-right-radius: var(
+ --bs-border-radius-xl
+ ) !important;
+}
+.rounded-end-5 {
+ border-top-right-radius: var(--bs-border-radius-xxl) !important;
+ border-bottom-right-radius: var(
+ --bs-border-radius-xxl
+ ) !important;
+}
+.rounded-end-circle {
+ border-top-right-radius: 50% !important;
+ border-bottom-right-radius: 50% !important;
+}
+.rounded-end-pill {
+ border-top-right-radius: var(
+ --bs-border-radius-pill
+ ) !important;
+ border-bottom-right-radius: var(
+ --bs-border-radius-pill
+ ) !important;
+}
+.rounded-bottom {
+ border-bottom-right-radius: var(--bs-border-radius) !important;
+ border-bottom-left-radius: var(--bs-border-radius) !important;
+}
+.rounded-bottom-0 {
+ border-bottom-right-radius: 0 !important;
+ border-bottom-left-radius: 0 !important;
+}
+.rounded-bottom-1 {
+ border-bottom-right-radius: var(
+ --bs-border-radius-sm
+ ) !important;
+ border-bottom-left-radius: var(
+ --bs-border-radius-sm
+ ) !important;
+}
+.rounded-bottom-2 {
+ border-bottom-right-radius: var(--bs-border-radius) !important;
+ border-bottom-left-radius: var(--bs-border-radius) !important;
+}
+.rounded-bottom-3 {
+ border-bottom-right-radius: var(
+ --bs-border-radius-lg
+ ) !important;
+ border-bottom-left-radius: var(
+ --bs-border-radius-lg
+ ) !important;
+}
+.rounded-bottom-4 {
+ border-bottom-right-radius: var(
+ --bs-border-radius-xl
+ ) !important;
+ border-bottom-left-radius: var(
+ --bs-border-radius-xl
+ ) !important;
+}
+.rounded-bottom-5 {
+ border-bottom-right-radius: var(
+ --bs-border-radius-xxl
+ ) !important;
+ border-bottom-left-radius: var(
+ --bs-border-radius-xxl
+ ) !important;
+}
+.rounded-bottom-circle {
+ border-bottom-right-radius: 50% !important;
+ border-bottom-left-radius: 50% !important;
+}
+.rounded-bottom-pill {
+ border-bottom-right-radius: var(
+ --bs-border-radius-pill
+ ) !important;
+ border-bottom-left-radius: var(
+ --bs-border-radius-pill
+ ) !important;
+}
+.rounded-start {
+ border-bottom-left-radius: var(--bs-border-radius) !important;
+ border-top-left-radius: var(--bs-border-radius) !important;
+}
+.rounded-start-0 {
+ border-bottom-left-radius: 0 !important;
+ border-top-left-radius: 0 !important;
+}
+.rounded-start-1 {
+ border-bottom-left-radius: var(
+ --bs-border-radius-sm
+ ) !important;
+ border-top-left-radius: var(--bs-border-radius-sm) !important;
+}
+.rounded-start-2 {
+ border-bottom-left-radius: var(--bs-border-radius) !important;
+ border-top-left-radius: var(--bs-border-radius) !important;
+}
+.rounded-start-3 {
+ border-bottom-left-radius: var(
+ --bs-border-radius-lg
+ ) !important;
+ border-top-left-radius: var(--bs-border-radius-lg) !important;
+}
+.rounded-start-4 {
+ border-bottom-left-radius: var(
+ --bs-border-radius-xl
+ ) !important;
+ border-top-left-radius: var(--bs-border-radius-xl) !important;
+}
+.rounded-start-5 {
+ border-bottom-left-radius: var(
+ --bs-border-radius-xxl
+ ) !important;
+ border-top-left-radius: var(--bs-border-radius-xxl) !important;
+}
+.rounded-start-circle {
+ border-bottom-left-radius: 50% !important;
+ border-top-left-radius: 50% !important;
+}
+.rounded-start-pill {
+ border-bottom-left-radius: var(
+ --bs-border-radius-pill
+ ) !important;
+ border-top-left-radius: var(--bs-border-radius-pill) !important;
+}
+.visible {
+ visibility: visible !important;
+}
+.invisible {
+ visibility: hidden !important;
+}
+.z-n1 {
+ z-index: -1 !important;
+}
+.z-0 {
+ z-index: 0 !important;
+}
+.z-1 {
+ z-index: 1 !important;
+}
+.z-2 {
+ z-index: 2 !important;
+}
+.z-3 {
+ z-index: 3 !important;
+}
+@media (min-width: 576px) {
+ .float-sm-start {
+ float: left !important;
+ }
+ .float-sm-end {
+ float: right !important;
+ }
+ .float-sm-none {
+ float: none !important;
+ }
+ .object-fit-sm-contain {
+ -o-object-fit: contain !important;
+ object-fit: contain !important;
+ }
+ .object-fit-sm-cover {
+ -o-object-fit: cover !important;
+ object-fit: cover !important;
+ }
+ .object-fit-sm-fill {
+ -o-object-fit: fill !important;
+ object-fit: fill !important;
+ }
+ .object-fit-sm-scale {
+ -o-object-fit: scale-down !important;
+ object-fit: scale-down !important;
+ }
+ .object-fit-sm-none {
+ -o-object-fit: none !important;
+ object-fit: none !important;
+ }
+ .d-sm-inline {
+ display: inline !important;
+ }
+ .d-sm-inline-block {
+ display: inline-block !important;
+ }
+ .d-sm-block {
+ display: block !important;
+ }
+ .d-sm-grid {
+ display: grid !important;
+ }
+ .d-sm-inline-grid {
+ display: inline-grid !important;
+ }
+ .d-sm-table {
+ display: table !important;
+ }
+ .d-sm-table-row {
+ display: table-row !important;
+ }
+ .d-sm-table-cell {
+ display: table-cell !important;
+ }
+ .d-sm-flex {
+ display: flex !important;
+ }
+ .d-sm-inline-flex {
+ display: inline-flex !important;
+ }
+ .d-sm-none {
+ display: none !important;
+ }
+ .flex-sm-fill {
+ flex: 1 1 auto !important;
+ }
+ .flex-sm-row {
+ flex-direction: row !important;
+ }
+ .flex-sm-column {
+ flex-direction: column !important;
+ }
+ .flex-sm-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+ .flex-sm-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+ .flex-sm-grow-0 {
+ flex-grow: 0 !important;
+ }
+ .flex-sm-grow-1 {
+ flex-grow: 1 !important;
+ }
+ .flex-sm-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+ .flex-sm-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+ .flex-sm-wrap {
+ flex-wrap: wrap !important;
+ }
+ .flex-sm-nowrap {
+ flex-wrap: nowrap !important;
+ }
+ .flex-sm-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+ .justify-content-sm-start {
+ justify-content: flex-start !important;
+ }
+ .justify-content-sm-end {
+ justify-content: flex-end !important;
+ }
+ .justify-content-sm-center {
+ justify-content: center !important;
+ }
+ .justify-content-sm-between {
+ justify-content: space-between !important;
+ }
+ .justify-content-sm-around {
+ justify-content: space-around !important;
+ }
+ .justify-content-sm-evenly {
+ justify-content: space-evenly !important;
+ }
+ .align-items-sm-start {
+ align-items: flex-start !important;
+ }
+ .align-items-sm-end {
+ align-items: flex-end !important;
+ }
+ .align-items-sm-center {
+ align-items: center !important;
+ }
+ .align-items-sm-baseline {
+ align-items: baseline !important;
+ }
+ .align-items-sm-stretch {
+ align-items: stretch !important;
+ }
+ .align-content-sm-start {
+ align-content: flex-start !important;
+ }
+ .align-content-sm-end {
+ align-content: flex-end !important;
+ }
+ .align-content-sm-center {
+ align-content: center !important;
+ }
+ .align-content-sm-between {
+ align-content: space-between !important;
+ }
+ .align-content-sm-around {
+ align-content: space-around !important;
+ }
+ .align-content-sm-stretch {
+ align-content: stretch !important;
+ }
+ .align-self-sm-auto {
+ align-self: auto !important;
+ }
+ .align-self-sm-start {
+ align-self: flex-start !important;
+ }
+ .align-self-sm-end {
+ align-self: flex-end !important;
+ }
+ .align-self-sm-center {
+ align-self: center !important;
+ }
+ .align-self-sm-baseline {
+ align-self: baseline !important;
+ }
+ .align-self-sm-stretch {
+ align-self: stretch !important;
+ }
+ .order-sm-first {
+ order: -1 !important;
+ }
+ .order-sm-0 {
+ order: 0 !important;
+ }
+ .order-sm-1 {
+ order: 1 !important;
+ }
+ .order-sm-2 {
+ order: 2 !important;
+ }
+ .order-sm-3 {
+ order: 3 !important;
+ }
+ .order-sm-4 {
+ order: 4 !important;
+ }
+ .order-sm-5 {
+ order: 5 !important;
+ }
+ .order-sm-last {
+ order: 6 !important;
+ }
+ .m-sm-0 {
+ margin: 0 !important;
+ }
+ .m-sm-1 {
+ margin: 0.25rem !important;
+ }
+ .m-sm-2 {
+ margin: 0.5rem !important;
+ }
+ .m-sm-3 {
+ margin: 1rem !important;
+ }
+ .m-sm-4 {
+ margin: 1.5rem !important;
+ }
+ .m-sm-5 {
+ margin: 3rem !important;
+ }
+ .m-sm-auto {
+ margin: auto !important;
+ }
+ .mx-sm-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
+ .mx-sm-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+ }
+ .mx-sm-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+ }
+ .mx-sm-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+ }
+ .mx-sm-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+ }
+ .mx-sm-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+ }
+ .mx-sm-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+ }
+ .my-sm-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+ .my-sm-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+ }
+ .my-sm-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+ .my-sm-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+ }
+ .my-sm-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+ }
+ .my-sm-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+ }
+ .my-sm-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ }
+ .mt-sm-0 {
+ margin-top: 0 !important;
+ }
+ .mt-sm-1 {
+ margin-top: 0.25rem !important;
+ }
+ .mt-sm-2 {
+ margin-top: 0.5rem !important;
+ }
+ .mt-sm-3 {
+ margin-top: 1rem !important;
+ }
+ .mt-sm-4 {
+ margin-top: 1.5rem !important;
+ }
+ .mt-sm-5 {
+ margin-top: 3rem !important;
+ }
+ .mt-sm-auto {
+ margin-top: auto !important;
+ }
+ .me-sm-0 {
+ margin-right: 0 !important;
+ }
+ .me-sm-1 {
+ margin-right: 0.25rem !important;
+ }
+ .me-sm-2 {
+ margin-right: 0.5rem !important;
+ }
+ .me-sm-3 {
+ margin-right: 1rem !important;
+ }
+ .me-sm-4 {
+ margin-right: 1.5rem !important;
+ }
+ .me-sm-5 {
+ margin-right: 3rem !important;
+ }
+ .me-sm-auto {
+ margin-right: auto !important;
+ }
+ .mb-sm-0 {
+ margin-bottom: 0 !important;
+ }
+ .mb-sm-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ .mb-sm-2 {
+ margin-bottom: 0.5rem !important;
+ }
+ .mb-sm-3 {
+ margin-bottom: 1rem !important;
+ }
+ .mb-sm-4 {
+ margin-bottom: 1.5rem !important;
+ }
+ .mb-sm-5 {
+ margin-bottom: 3rem !important;
+ }
+ .mb-sm-auto {
+ margin-bottom: auto !important;
+ }
+ .ms-sm-0 {
+ margin-left: 0 !important;
+ }
+ .ms-sm-1 {
+ margin-left: 0.25rem !important;
+ }
+ .ms-sm-2 {
+ margin-left: 0.5rem !important;
+ }
+ .ms-sm-3 {
+ margin-left: 1rem !important;
+ }
+ .ms-sm-4 {
+ margin-left: 1.5rem !important;
+ }
+ .ms-sm-5 {
+ margin-left: 3rem !important;
+ }
+ .ms-sm-auto {
+ margin-left: auto !important;
+ }
+ .p-sm-0 {
+ padding: 0 !important;
+ }
+ .p-sm-1 {
+ padding: 0.25rem !important;
+ }
+ .p-sm-2 {
+ padding: 0.5rem !important;
+ }
+ .p-sm-3 {
+ padding: 1rem !important;
+ }
+ .p-sm-4 {
+ padding: 1.5rem !important;
+ }
+ .p-sm-5 {
+ padding: 3rem !important;
+ }
+ .px-sm-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+ .px-sm-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+ }
+ .px-sm-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+ }
+ .px-sm-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+ }
+ .px-sm-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+ }
+ .px-sm-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+ }
+ .py-sm-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ }
+ .py-sm-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+ .py-sm-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+ .py-sm-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+ }
+ .py-sm-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+ }
+ .py-sm-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+ }
+ .pt-sm-0 {
+ padding-top: 0 !important;
+ }
+ .pt-sm-1 {
+ padding-top: 0.25rem !important;
+ }
+ .pt-sm-2 {
+ padding-top: 0.5rem !important;
+ }
+ .pt-sm-3 {
+ padding-top: 1rem !important;
+ }
+ .pt-sm-4 {
+ padding-top: 1.5rem !important;
+ }
+ .pt-sm-5 {
+ padding-top: 3rem !important;
+ }
+ .pe-sm-0 {
+ padding-right: 0 !important;
+ }
+ .pe-sm-1 {
+ padding-right: 0.25rem !important;
+ }
+ .pe-sm-2 {
+ padding-right: 0.5rem !important;
+ }
+ .pe-sm-3 {
+ padding-right: 1rem !important;
+ }
+ .pe-sm-4 {
+ padding-right: 1.5rem !important;
+ }
+ .pe-sm-5 {
+ padding-right: 3rem !important;
+ }
+ .pb-sm-0 {
+ padding-bottom: 0 !important;
+ }
+ .pb-sm-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ .pb-sm-2 {
+ padding-bottom: 0.5rem !important;
+ }
+ .pb-sm-3 {
+ padding-bottom: 1rem !important;
+ }
+ .pb-sm-4 {
+ padding-bottom: 1.5rem !important;
+ }
+ .pb-sm-5 {
+ padding-bottom: 3rem !important;
+ }
+ .ps-sm-0 {
+ padding-left: 0 !important;
+ }
+ .ps-sm-1 {
+ padding-left: 0.25rem !important;
+ }
+ .ps-sm-2 {
+ padding-left: 0.5rem !important;
+ }
+ .ps-sm-3 {
+ padding-left: 1rem !important;
+ }
+ .ps-sm-4 {
+ padding-left: 1.5rem !important;
+ }
+ .ps-sm-5 {
+ padding-left: 3rem !important;
+ }
+ .gap-sm-0 {
+ gap: 0 !important;
+ }
+ .gap-sm-1 {
+ gap: 0.25rem !important;
+ }
+ .gap-sm-2 {
+ gap: 0.5rem !important;
+ }
+ .gap-sm-3 {
+ gap: 1rem !important;
+ }
+ .gap-sm-4 {
+ gap: 1.5rem !important;
+ }
+ .gap-sm-5 {
+ gap: 3rem !important;
+ }
+ .row-gap-sm-0 {
+ row-gap: 0 !important;
+ }
+ .row-gap-sm-1 {
+ row-gap: 0.25rem !important;
+ }
+ .row-gap-sm-2 {
+ row-gap: 0.5rem !important;
+ }
+ .row-gap-sm-3 {
+ row-gap: 1rem !important;
+ }
+ .row-gap-sm-4 {
+ row-gap: 1.5rem !important;
+ }
+ .row-gap-sm-5 {
+ row-gap: 3rem !important;
+ }
+ .column-gap-sm-0 {
+ -moz-column-gap: 0 !important;
+ column-gap: 0 !important;
+ }
+ .column-gap-sm-1 {
+ -moz-column-gap: 0.25rem !important;
+ column-gap: 0.25rem !important;
+ }
+ .column-gap-sm-2 {
+ -moz-column-gap: 0.5rem !important;
+ column-gap: 0.5rem !important;
+ }
+ .column-gap-sm-3 {
+ -moz-column-gap: 1rem !important;
+ column-gap: 1rem !important;
+ }
+ .column-gap-sm-4 {
+ -moz-column-gap: 1.5rem !important;
+ column-gap: 1.5rem !important;
+ }
+ .column-gap-sm-5 {
+ -moz-column-gap: 3rem !important;
+ column-gap: 3rem !important;
+ }
+ .text-sm-start {
+ text-align: left !important;
+ }
+ .text-sm-end {
+ text-align: right !important;
+ }
+ .text-sm-center {
+ text-align: center !important;
+ }
+}
+@media (min-width: 768px) {
+ .float-md-start {
+ float: left !important;
+ }
+ .float-md-end {
+ float: right !important;
+ }
+ .float-md-none {
+ float: none !important;
+ }
+ .object-fit-md-contain {
+ -o-object-fit: contain !important;
+ object-fit: contain !important;
+ }
+ .object-fit-md-cover {
+ -o-object-fit: cover !important;
+ object-fit: cover !important;
+ }
+ .object-fit-md-fill {
+ -o-object-fit: fill !important;
+ object-fit: fill !important;
+ }
+ .object-fit-md-scale {
+ -o-object-fit: scale-down !important;
+ object-fit: scale-down !important;
+ }
+ .object-fit-md-none {
+ -o-object-fit: none !important;
+ object-fit: none !important;
+ }
+ .d-md-inline {
+ display: inline !important;
+ }
+ .d-md-inline-block {
+ display: inline-block !important;
+ }
+ .d-md-block {
+ display: block !important;
+ }
+ .d-md-grid {
+ display: grid !important;
+ }
+ .d-md-inline-grid {
+ display: inline-grid !important;
+ }
+ .d-md-table {
+ display: table !important;
+ }
+ .d-md-table-row {
+ display: table-row !important;
+ }
+ .d-md-table-cell {
+ display: table-cell !important;
+ }
+ .d-md-flex {
+ display: flex !important;
+ }
+ .d-md-inline-flex {
+ display: inline-flex !important;
+ }
+ .d-md-none {
+ display: none !important;
+ }
+ .flex-md-fill {
+ flex: 1 1 auto !important;
+ }
+ .flex-md-row {
+ flex-direction: row !important;
+ }
+ .flex-md-column {
+ flex-direction: column !important;
+ }
+ .flex-md-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+ .flex-md-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+ .flex-md-grow-0 {
+ flex-grow: 0 !important;
+ }
+ .flex-md-grow-1 {
+ flex-grow: 1 !important;
+ }
+ .flex-md-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+ .flex-md-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+ .flex-md-wrap {
+ flex-wrap: wrap !important;
+ }
+ .flex-md-nowrap {
+ flex-wrap: nowrap !important;
+ }
+ .flex-md-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+ .justify-content-md-start {
+ justify-content: flex-start !important;
+ }
+ .justify-content-md-end {
+ justify-content: flex-end !important;
+ }
+ .justify-content-md-center {
+ justify-content: center !important;
+ }
+ .justify-content-md-between {
+ justify-content: space-between !important;
+ }
+ .justify-content-md-around {
+ justify-content: space-around !important;
+ }
+ .justify-content-md-evenly {
+ justify-content: space-evenly !important;
+ }
+ .align-items-md-start {
+ align-items: flex-start !important;
+ }
+ .align-items-md-end {
+ align-items: flex-end !important;
+ }
+ .align-items-md-center {
+ align-items: center !important;
+ }
+ .align-items-md-baseline {
+ align-items: baseline !important;
+ }
+ .align-items-md-stretch {
+ align-items: stretch !important;
+ }
+ .align-content-md-start {
+ align-content: flex-start !important;
+ }
+ .align-content-md-end {
+ align-content: flex-end !important;
+ }
+ .align-content-md-center {
+ align-content: center !important;
+ }
+ .align-content-md-between {
+ align-content: space-between !important;
+ }
+ .align-content-md-around {
+ align-content: space-around !important;
+ }
+ .align-content-md-stretch {
+ align-content: stretch !important;
+ }
+ .align-self-md-auto {
+ align-self: auto !important;
+ }
+ .align-self-md-start {
+ align-self: flex-start !important;
+ }
+ .align-self-md-end {
+ align-self: flex-end !important;
+ }
+ .align-self-md-center {
+ align-self: center !important;
+ }
+ .align-self-md-baseline {
+ align-self: baseline !important;
+ }
+ .align-self-md-stretch {
+ align-self: stretch !important;
+ }
+ .order-md-first {
+ order: -1 !important;
+ }
+ .order-md-0 {
+ order: 0 !important;
+ }
+ .order-md-1 {
+ order: 1 !important;
+ }
+ .order-md-2 {
+ order: 2 !important;
+ }
+ .order-md-3 {
+ order: 3 !important;
+ }
+ .order-md-4 {
+ order: 4 !important;
+ }
+ .order-md-5 {
+ order: 5 !important;
+ }
+ .order-md-last {
+ order: 6 !important;
+ }
+ .m-md-0 {
+ margin: 0 !important;
+ }
+ .m-md-1 {
+ margin: 0.25rem !important;
+ }
+ .m-md-2 {
+ margin: 0.5rem !important;
+ }
+ .m-md-3 {
+ margin: 1rem !important;
+ }
+ .m-md-4 {
+ margin: 1.5rem !important;
+ }
+ .m-md-5 {
+ margin: 3rem !important;
+ }
+ .m-md-auto {
+ margin: auto !important;
+ }
+ .mx-md-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
+ .mx-md-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+ }
+ .mx-md-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+ }
+ .mx-md-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+ }
+ .mx-md-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+ }
+ .mx-md-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+ }
+ .mx-md-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+ }
+ .my-md-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+ .my-md-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+ }
+ .my-md-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+ .my-md-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+ }
+ .my-md-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+ }
+ .my-md-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+ }
+ .my-md-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ }
+ .mt-md-0 {
+ margin-top: 0 !important;
+ }
+ .mt-md-1 {
+ margin-top: 0.25rem !important;
+ }
+ .mt-md-2 {
+ margin-top: 0.5rem !important;
+ }
+ .mt-md-3 {
+ margin-top: 1rem !important;
+ }
+ .mt-md-4 {
+ margin-top: 1.5rem !important;
+ }
+ .mt-md-5 {
+ margin-top: 3rem !important;
+ }
+ .mt-md-auto {
+ margin-top: auto !important;
+ }
+ .me-md-0 {
+ margin-right: 0 !important;
+ }
+ .me-md-1 {
+ margin-right: 0.25rem !important;
+ }
+ .me-md-2 {
+ margin-right: 0.5rem !important;
+ }
+ .me-md-3 {
+ margin-right: 1rem !important;
+ }
+ .me-md-4 {
+ margin-right: 1.5rem !important;
+ }
+ .me-md-5 {
+ margin-right: 3rem !important;
+ }
+ .me-md-auto {
+ margin-right: auto !important;
+ }
+ .mb-md-0 {
+ margin-bottom: 0 !important;
+ }
+ .mb-md-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ .mb-md-2 {
+ margin-bottom: 0.5rem !important;
+ }
+ .mb-md-3 {
+ margin-bottom: 1rem !important;
+ }
+ .mb-md-4 {
+ margin-bottom: 1.5rem !important;
+ }
+ .mb-md-5 {
+ margin-bottom: 3rem !important;
+ }
+ .mb-md-auto {
+ margin-bottom: auto !important;
+ }
+ .ms-md-0 {
+ margin-left: 0 !important;
+ }
+ .ms-md-1 {
+ margin-left: 0.25rem !important;
+ }
+ .ms-md-2 {
+ margin-left: 0.5rem !important;
+ }
+ .ms-md-3 {
+ margin-left: 1rem !important;
+ }
+ .ms-md-4 {
+ margin-left: 1.5rem !important;
+ }
+ .ms-md-5 {
+ margin-left: 3rem !important;
+ }
+ .ms-md-auto {
+ margin-left: auto !important;
+ }
+ .p-md-0 {
+ padding: 0 !important;
+ }
+ .p-md-1 {
+ padding: 0.25rem !important;
+ }
+ .p-md-2 {
+ padding: 0.5rem !important;
+ }
+ .p-md-3 {
+ padding: 1rem !important;
+ }
+ .p-md-4 {
+ padding: 1.5rem !important;
+ }
+ .p-md-5 {
+ padding: 3rem !important;
+ }
+ .px-md-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+ .px-md-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+ }
+ .px-md-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+ }
+ .px-md-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+ }
+ .px-md-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+ }
+ .px-md-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+ }
+ .py-md-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ }
+ .py-md-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+ .py-md-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+ .py-md-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+ }
+ .py-md-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+ }
+ .py-md-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+ }
+ .pt-md-0 {
+ padding-top: 0 !important;
+ }
+ .pt-md-1 {
+ padding-top: 0.25rem !important;
+ }
+ .pt-md-2 {
+ padding-top: 0.5rem !important;
+ }
+ .pt-md-3 {
+ padding-top: 1rem !important;
+ }
+ .pt-md-4 {
+ padding-top: 1.5rem !important;
+ }
+ .pt-md-5 {
+ padding-top: 3rem !important;
+ }
+ .pe-md-0 {
+ padding-right: 0 !important;
+ }
+ .pe-md-1 {
+ padding-right: 0.25rem !important;
+ }
+ .pe-md-2 {
+ padding-right: 0.5rem !important;
+ }
+ .pe-md-3 {
+ padding-right: 1rem !important;
+ }
+ .pe-md-4 {
+ padding-right: 1.5rem !important;
+ }
+ .pe-md-5 {
+ padding-right: 3rem !important;
+ }
+ .pb-md-0 {
+ padding-bottom: 0 !important;
+ }
+ .pb-md-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ .pb-md-2 {
+ padding-bottom: 0.5rem !important;
+ }
+ .pb-md-3 {
+ padding-bottom: 1rem !important;
+ }
+ .pb-md-4 {
+ padding-bottom: 1.5rem !important;
+ }
+ .pb-md-5 {
+ padding-bottom: 3rem !important;
+ }
+ .ps-md-0 {
+ padding-left: 0 !important;
+ }
+ .ps-md-1 {
+ padding-left: 0.25rem !important;
+ }
+ .ps-md-2 {
+ padding-left: 0.5rem !important;
+ }
+ .ps-md-3 {
+ padding-left: 1rem !important;
+ }
+ .ps-md-4 {
+ padding-left: 1.5rem !important;
+ }
+ .ps-md-5 {
+ padding-left: 3rem !important;
+ }
+ .gap-md-0 {
+ gap: 0 !important;
+ }
+ .gap-md-1 {
+ gap: 0.25rem !important;
+ }
+ .gap-md-2 {
+ gap: 0.5rem !important;
+ }
+ .gap-md-3 {
+ gap: 1rem !important;
+ }
+ .gap-md-4 {
+ gap: 1.5rem !important;
+ }
+ .gap-md-5 {
+ gap: 3rem !important;
+ }
+ .row-gap-md-0 {
+ row-gap: 0 !important;
+ }
+ .row-gap-md-1 {
+ row-gap: 0.25rem !important;
+ }
+ .row-gap-md-2 {
+ row-gap: 0.5rem !important;
+ }
+ .row-gap-md-3 {
+ row-gap: 1rem !important;
+ }
+ .row-gap-md-4 {
+ row-gap: 1.5rem !important;
+ }
+ .row-gap-md-5 {
+ row-gap: 3rem !important;
+ }
+ .column-gap-md-0 {
+ -moz-column-gap: 0 !important;
+ column-gap: 0 !important;
+ }
+ .column-gap-md-1 {
+ -moz-column-gap: 0.25rem !important;
+ column-gap: 0.25rem !important;
+ }
+ .column-gap-md-2 {
+ -moz-column-gap: 0.5rem !important;
+ column-gap: 0.5rem !important;
+ }
+ .column-gap-md-3 {
+ -moz-column-gap: 1rem !important;
+ column-gap: 1rem !important;
+ }
+ .column-gap-md-4 {
+ -moz-column-gap: 1.5rem !important;
+ column-gap: 1.5rem !important;
+ }
+ .column-gap-md-5 {
+ -moz-column-gap: 3rem !important;
+ column-gap: 3rem !important;
+ }
+ .text-md-start {
+ text-align: left !important;
+ }
+ .text-md-end {
+ text-align: right !important;
+ }
+ .text-md-center {
+ text-align: center !important;
+ }
+}
+@media (min-width: 992px) {
+ .float-lg-start {
+ float: left !important;
+ }
+ .float-lg-end {
+ float: right !important;
+ }
+ .float-lg-none {
+ float: none !important;
+ }
+ .object-fit-lg-contain {
+ -o-object-fit: contain !important;
+ object-fit: contain !important;
+ }
+ .object-fit-lg-cover {
+ -o-object-fit: cover !important;
+ object-fit: cover !important;
+ }
+ .object-fit-lg-fill {
+ -o-object-fit: fill !important;
+ object-fit: fill !important;
+ }
+ .object-fit-lg-scale {
+ -o-object-fit: scale-down !important;
+ object-fit: scale-down !important;
+ }
+ .object-fit-lg-none {
+ -o-object-fit: none !important;
+ object-fit: none !important;
+ }
+ .d-lg-inline {
+ display: inline !important;
+ }
+ .d-lg-inline-block {
+ display: inline-block !important;
+ }
+ .d-lg-block {
+ display: block !important;
+ }
+ .d-lg-grid {
+ display: grid !important;
+ }
+ .d-lg-inline-grid {
+ display: inline-grid !important;
+ }
+ .d-lg-table {
+ display: table !important;
+ }
+ .d-lg-table-row {
+ display: table-row !important;
+ }
+ .d-lg-table-cell {
+ display: table-cell !important;
+ }
+ .d-lg-flex {
+ display: flex !important;
+ }
+ .d-lg-inline-flex {
+ display: inline-flex !important;
+ }
+ .d-lg-none {
+ display: none !important;
+ }
+ .flex-lg-fill {
+ flex: 1 1 auto !important;
+ }
+ .flex-lg-row {
+ flex-direction: row !important;
+ }
+ .flex-lg-column {
+ flex-direction: column !important;
+ }
+ .flex-lg-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+ .flex-lg-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+ .flex-lg-grow-0 {
+ flex-grow: 0 !important;
+ }
+ .flex-lg-grow-1 {
+ flex-grow: 1 !important;
+ }
+ .flex-lg-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+ .flex-lg-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+ .flex-lg-wrap {
+ flex-wrap: wrap !important;
+ }
+ .flex-lg-nowrap {
+ flex-wrap: nowrap !important;
+ }
+ .flex-lg-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+ .justify-content-lg-start {
+ justify-content: flex-start !important;
+ }
+ .justify-content-lg-end {
+ justify-content: flex-end !important;
+ }
+ .justify-content-lg-center {
+ justify-content: center !important;
+ }
+ .justify-content-lg-between {
+ justify-content: space-between !important;
+ }
+ .justify-content-lg-around {
+ justify-content: space-around !important;
+ }
+ .justify-content-lg-evenly {
+ justify-content: space-evenly !important;
+ }
+ .align-items-lg-start {
+ align-items: flex-start !important;
+ }
+ .align-items-lg-end {
+ align-items: flex-end !important;
+ }
+ .align-items-lg-center {
+ align-items: center !important;
+ }
+ .align-items-lg-baseline {
+ align-items: baseline !important;
+ }
+ .align-items-lg-stretch {
+ align-items: stretch !important;
+ }
+ .align-content-lg-start {
+ align-content: flex-start !important;
+ }
+ .align-content-lg-end {
+ align-content: flex-end !important;
+ }
+ .align-content-lg-center {
+ align-content: center !important;
+ }
+ .align-content-lg-between {
+ align-content: space-between !important;
+ }
+ .align-content-lg-around {
+ align-content: space-around !important;
+ }
+ .align-content-lg-stretch {
+ align-content: stretch !important;
+ }
+ .align-self-lg-auto {
+ align-self: auto !important;
+ }
+ .align-self-lg-start {
+ align-self: flex-start !important;
+ }
+ .align-self-lg-end {
+ align-self: flex-end !important;
+ }
+ .align-self-lg-center {
+ align-self: center !important;
+ }
+ .align-self-lg-baseline {
+ align-self: baseline !important;
+ }
+ .align-self-lg-stretch {
+ align-self: stretch !important;
+ }
+ .order-lg-first {
+ order: -1 !important;
+ }
+ .order-lg-0 {
+ order: 0 !important;
+ }
+ .order-lg-1 {
+ order: 1 !important;
+ }
+ .order-lg-2 {
+ order: 2 !important;
+ }
+ .order-lg-3 {
+ order: 3 !important;
+ }
+ .order-lg-4 {
+ order: 4 !important;
+ }
+ .order-lg-5 {
+ order: 5 !important;
+ }
+ .order-lg-last {
+ order: 6 !important;
+ }
+ .m-lg-0 {
+ margin: 0 !important;
+ }
+ .m-lg-1 {
+ margin: 0.25rem !important;
+ }
+ .m-lg-2 {
+ margin: 0.5rem !important;
+ }
+ .m-lg-3 {
+ margin: 1rem !important;
+ }
+ .m-lg-4 {
+ margin: 1.5rem !important;
+ }
+ .m-lg-5 {
+ margin: 3rem !important;
+ }
+ .m-lg-auto {
+ margin: auto !important;
+ }
+ .mx-lg-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
+ .mx-lg-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+ }
+ .mx-lg-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+ }
+ .mx-lg-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+ }
+ .mx-lg-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+ }
+ .mx-lg-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+ }
+ .mx-lg-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+ }
+ .my-lg-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+ .my-lg-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+ }
+ .my-lg-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+ .my-lg-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+ }
+ .my-lg-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+ }
+ .my-lg-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+ }
+ .my-lg-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ }
+ .mt-lg-0 {
+ margin-top: 0 !important;
+ }
+ .mt-lg-1 {
+ margin-top: 0.25rem !important;
+ }
+ .mt-lg-2 {
+ margin-top: 0.5rem !important;
+ }
+ .mt-lg-3 {
+ margin-top: 1rem !important;
+ }
+ .mt-lg-4 {
+ margin-top: 1.5rem !important;
+ }
+ .mt-lg-5 {
+ margin-top: 3rem !important;
+ }
+ .mt-lg-auto {
+ margin-top: auto !important;
+ }
+ .me-lg-0 {
+ margin-right: 0 !important;
+ }
+ .me-lg-1 {
+ margin-right: 0.25rem !important;
+ }
+ .me-lg-2 {
+ margin-right: 0.5rem !important;
+ }
+ .me-lg-3 {
+ margin-right: 1rem !important;
+ }
+ .me-lg-4 {
+ margin-right: 1.5rem !important;
+ }
+ .me-lg-5 {
+ margin-right: 3rem !important;
+ }
+ .me-lg-auto {
+ margin-right: auto !important;
+ }
+ .mb-lg-0 {
+ margin-bottom: 0 !important;
+ }
+ .mb-lg-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ .mb-lg-2 {
+ margin-bottom: 0.5rem !important;
+ }
+ .mb-lg-3 {
+ margin-bottom: 1rem !important;
+ }
+ .mb-lg-4 {
+ margin-bottom: 1.5rem !important;
+ }
+ .mb-lg-5 {
+ margin-bottom: 3rem !important;
+ }
+ .mb-lg-auto {
+ margin-bottom: auto !important;
+ }
+ .ms-lg-0 {
+ margin-left: 0 !important;
+ }
+ .ms-lg-1 {
+ margin-left: 0.25rem !important;
+ }
+ .ms-lg-2 {
+ margin-left: 0.5rem !important;
+ }
+ .ms-lg-3 {
+ margin-left: 1rem !important;
+ }
+ .ms-lg-4 {
+ margin-left: 1.5rem !important;
+ }
+ .ms-lg-5 {
+ margin-left: 3rem !important;
+ }
+ .ms-lg-auto {
+ margin-left: auto !important;
+ }
+ .p-lg-0 {
+ padding: 0 !important;
+ }
+ .p-lg-1 {
+ padding: 0.25rem !important;
+ }
+ .p-lg-2 {
+ padding: 0.5rem !important;
+ }
+ .p-lg-3 {
+ padding: 1rem !important;
+ }
+ .p-lg-4 {
+ padding: 1.5rem !important;
+ }
+ .p-lg-5 {
+ padding: 3rem !important;
+ }
+ .px-lg-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+ .px-lg-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+ }
+ .px-lg-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+ }
+ .px-lg-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+ }
+ .px-lg-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+ }
+ .px-lg-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+ }
+ .py-lg-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ }
+ .py-lg-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+ .py-lg-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+ .py-lg-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+ }
+ .py-lg-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+ }
+ .py-lg-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+ }
+ .pt-lg-0 {
+ padding-top: 0 !important;
+ }
+ .pt-lg-1 {
+ padding-top: 0.25rem !important;
+ }
+ .pt-lg-2 {
+ padding-top: 0.5rem !important;
+ }
+ .pt-lg-3 {
+ padding-top: 1rem !important;
+ }
+ .pt-lg-4 {
+ padding-top: 1.5rem !important;
+ }
+ .pt-lg-5 {
+ padding-top: 3rem !important;
+ }
+ .pe-lg-0 {
+ padding-right: 0 !important;
+ }
+ .pe-lg-1 {
+ padding-right: 0.25rem !important;
+ }
+ .pe-lg-2 {
+ padding-right: 0.5rem !important;
+ }
+ .pe-lg-3 {
+ padding-right: 1rem !important;
+ }
+ .pe-lg-4 {
+ padding-right: 1.5rem !important;
+ }
+ .pe-lg-5 {
+ padding-right: 3rem !important;
+ }
+ .pb-lg-0 {
+ padding-bottom: 0 !important;
+ }
+ .pb-lg-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ .pb-lg-2 {
+ padding-bottom: 0.5rem !important;
+ }
+ .pb-lg-3 {
+ padding-bottom: 1rem !important;
+ }
+ .pb-lg-4 {
+ padding-bottom: 1.5rem !important;
+ }
+ .pb-lg-5 {
+ padding-bottom: 3rem !important;
+ }
+ .ps-lg-0 {
+ padding-left: 0 !important;
+ }
+ .ps-lg-1 {
+ padding-left: 0.25rem !important;
+ }
+ .ps-lg-2 {
+ padding-left: 0.5rem !important;
+ }
+ .ps-lg-3 {
+ padding-left: 1rem !important;
+ }
+ .ps-lg-4 {
+ padding-left: 1.5rem !important;
+ }
+ .ps-lg-5 {
+ padding-left: 3rem !important;
+ }
+ .gap-lg-0 {
+ gap: 0 !important;
+ }
+ .gap-lg-1 {
+ gap: 0.25rem !important;
+ }
+ .gap-lg-2 {
+ gap: 0.5rem !important;
+ }
+ .gap-lg-3 {
+ gap: 1rem !important;
+ }
+ .gap-lg-4 {
+ gap: 1.5rem !important;
+ }
+ .gap-lg-5 {
+ gap: 3rem !important;
+ }
+ .row-gap-lg-0 {
+ row-gap: 0 !important;
+ }
+ .row-gap-lg-1 {
+ row-gap: 0.25rem !important;
+ }
+ .row-gap-lg-2 {
+ row-gap: 0.5rem !important;
+ }
+ .row-gap-lg-3 {
+ row-gap: 1rem !important;
+ }
+ .row-gap-lg-4 {
+ row-gap: 1.5rem !important;
+ }
+ .row-gap-lg-5 {
+ row-gap: 3rem !important;
+ }
+ .column-gap-lg-0 {
+ -moz-column-gap: 0 !important;
+ column-gap: 0 !important;
+ }
+ .column-gap-lg-1 {
+ -moz-column-gap: 0.25rem !important;
+ column-gap: 0.25rem !important;
+ }
+ .column-gap-lg-2 {
+ -moz-column-gap: 0.5rem !important;
+ column-gap: 0.5rem !important;
+ }
+ .column-gap-lg-3 {
+ -moz-column-gap: 1rem !important;
+ column-gap: 1rem !important;
+ }
+ .column-gap-lg-4 {
+ -moz-column-gap: 1.5rem !important;
+ column-gap: 1.5rem !important;
+ }
+ .column-gap-lg-5 {
+ -moz-column-gap: 3rem !important;
+ column-gap: 3rem !important;
+ }
+ .text-lg-start {
+ text-align: left !important;
+ }
+ .text-lg-end {
+ text-align: right !important;
+ }
+ .text-lg-center {
+ text-align: center !important;
+ }
+}
+@media (min-width: 1200px) {
+ .float-xl-start {
+ float: left !important;
+ }
+ .float-xl-end {
+ float: right !important;
+ }
+ .float-xl-none {
+ float: none !important;
+ }
+ .object-fit-xl-contain {
+ -o-object-fit: contain !important;
+ object-fit: contain !important;
+ }
+ .object-fit-xl-cover {
+ -o-object-fit: cover !important;
+ object-fit: cover !important;
+ }
+ .object-fit-xl-fill {
+ -o-object-fit: fill !important;
+ object-fit: fill !important;
+ }
+ .object-fit-xl-scale {
+ -o-object-fit: scale-down !important;
+ object-fit: scale-down !important;
+ }
+ .object-fit-xl-none {
+ -o-object-fit: none !important;
+ object-fit: none !important;
+ }
+ .d-xl-inline {
+ display: inline !important;
+ }
+ .d-xl-inline-block {
+ display: inline-block !important;
+ }
+ .d-xl-block {
+ display: block !important;
+ }
+ .d-xl-grid {
+ display: grid !important;
+ }
+ .d-xl-inline-grid {
+ display: inline-grid !important;
+ }
+ .d-xl-table {
+ display: table !important;
+ }
+ .d-xl-table-row {
+ display: table-row !important;
+ }
+ .d-xl-table-cell {
+ display: table-cell !important;
+ }
+ .d-xl-flex {
+ display: flex !important;
+ }
+ .d-xl-inline-flex {
+ display: inline-flex !important;
+ }
+ .d-xl-none {
+ display: none !important;
+ }
+ .flex-xl-fill {
+ flex: 1 1 auto !important;
+ }
+ .flex-xl-row {
+ flex-direction: row !important;
+ }
+ .flex-xl-column {
+ flex-direction: column !important;
+ }
+ .flex-xl-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+ .flex-xl-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+ .flex-xl-grow-0 {
+ flex-grow: 0 !important;
+ }
+ .flex-xl-grow-1 {
+ flex-grow: 1 !important;
+ }
+ .flex-xl-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+ .flex-xl-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+ .flex-xl-wrap {
+ flex-wrap: wrap !important;
+ }
+ .flex-xl-nowrap {
+ flex-wrap: nowrap !important;
+ }
+ .flex-xl-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+ .justify-content-xl-start {
+ justify-content: flex-start !important;
+ }
+ .justify-content-xl-end {
+ justify-content: flex-end !important;
+ }
+ .justify-content-xl-center {
+ justify-content: center !important;
+ }
+ .justify-content-xl-between {
+ justify-content: space-between !important;
+ }
+ .justify-content-xl-around {
+ justify-content: space-around !important;
+ }
+ .justify-content-xl-evenly {
+ justify-content: space-evenly !important;
+ }
+ .align-items-xl-start {
+ align-items: flex-start !important;
+ }
+ .align-items-xl-end {
+ align-items: flex-end !important;
+ }
+ .align-items-xl-center {
+ align-items: center !important;
+ }
+ .align-items-xl-baseline {
+ align-items: baseline !important;
+ }
+ .align-items-xl-stretch {
+ align-items: stretch !important;
+ }
+ .align-content-xl-start {
+ align-content: flex-start !important;
+ }
+ .align-content-xl-end {
+ align-content: flex-end !important;
+ }
+ .align-content-xl-center {
+ align-content: center !important;
+ }
+ .align-content-xl-between {
+ align-content: space-between !important;
+ }
+ .align-content-xl-around {
+ align-content: space-around !important;
+ }
+ .align-content-xl-stretch {
+ align-content: stretch !important;
+ }
+ .align-self-xl-auto {
+ align-self: auto !important;
+ }
+ .align-self-xl-start {
+ align-self: flex-start !important;
+ }
+ .align-self-xl-end {
+ align-self: flex-end !important;
+ }
+ .align-self-xl-center {
+ align-self: center !important;
+ }
+ .align-self-xl-baseline {
+ align-self: baseline !important;
+ }
+ .align-self-xl-stretch {
+ align-self: stretch !important;
+ }
+ .order-xl-first {
+ order: -1 !important;
+ }
+ .order-xl-0 {
+ order: 0 !important;
+ }
+ .order-xl-1 {
+ order: 1 !important;
+ }
+ .order-xl-2 {
+ order: 2 !important;
+ }
+ .order-xl-3 {
+ order: 3 !important;
+ }
+ .order-xl-4 {
+ order: 4 !important;
+ }
+ .order-xl-5 {
+ order: 5 !important;
+ }
+ .order-xl-last {
+ order: 6 !important;
+ }
+ .m-xl-0 {
+ margin: 0 !important;
+ }
+ .m-xl-1 {
+ margin: 0.25rem !important;
+ }
+ .m-xl-2 {
+ margin: 0.5rem !important;
+ }
+ .m-xl-3 {
+ margin: 1rem !important;
+ }
+ .m-xl-4 {
+ margin: 1.5rem !important;
+ }
+ .m-xl-5 {
+ margin: 3rem !important;
+ }
+ .m-xl-auto {
+ margin: auto !important;
+ }
+ .mx-xl-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
+ .mx-xl-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+ }
+ .mx-xl-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+ }
+ .mx-xl-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+ }
+ .mx-xl-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+ }
+ .mx-xl-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+ }
+ .mx-xl-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+ }
+ .my-xl-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+ .my-xl-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+ }
+ .my-xl-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+ .my-xl-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+ }
+ .my-xl-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+ }
+ .my-xl-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+ }
+ .my-xl-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ }
+ .mt-xl-0 {
+ margin-top: 0 !important;
+ }
+ .mt-xl-1 {
+ margin-top: 0.25rem !important;
+ }
+ .mt-xl-2 {
+ margin-top: 0.5rem !important;
+ }
+ .mt-xl-3 {
+ margin-top: 1rem !important;
+ }
+ .mt-xl-4 {
+ margin-top: 1.5rem !important;
+ }
+ .mt-xl-5 {
+ margin-top: 3rem !important;
+ }
+ .mt-xl-auto {
+ margin-top: auto !important;
+ }
+ .me-xl-0 {
+ margin-right: 0 !important;
+ }
+ .me-xl-1 {
+ margin-right: 0.25rem !important;
+ }
+ .me-xl-2 {
+ margin-right: 0.5rem !important;
+ }
+ .me-xl-3 {
+ margin-right: 1rem !important;
+ }
+ .me-xl-4 {
+ margin-right: 1.5rem !important;
+ }
+ .me-xl-5 {
+ margin-right: 3rem !important;
+ }
+ .me-xl-auto {
+ margin-right: auto !important;
+ }
+ .mb-xl-0 {
+ margin-bottom: 0 !important;
+ }
+ .mb-xl-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ .mb-xl-2 {
+ margin-bottom: 0.5rem !important;
+ }
+ .mb-xl-3 {
+ margin-bottom: 1rem !important;
+ }
+ .mb-xl-4 {
+ margin-bottom: 1.5rem !important;
+ }
+ .mb-xl-5 {
+ margin-bottom: 3rem !important;
+ }
+ .mb-xl-auto {
+ margin-bottom: auto !important;
+ }
+ .ms-xl-0 {
+ margin-left: 0 !important;
+ }
+ .ms-xl-1 {
+ margin-left: 0.25rem !important;
+ }
+ .ms-xl-2 {
+ margin-left: 0.5rem !important;
+ }
+ .ms-xl-3 {
+ margin-left: 1rem !important;
+ }
+ .ms-xl-4 {
+ margin-left: 1.5rem !important;
+ }
+ .ms-xl-5 {
+ margin-left: 3rem !important;
+ }
+ .ms-xl-auto {
+ margin-left: auto !important;
+ }
+ .p-xl-0 {
+ padding: 0 !important;
+ }
+ .p-xl-1 {
+ padding: 0.25rem !important;
+ }
+ .p-xl-2 {
+ padding: 0.5rem !important;
+ }
+ .p-xl-3 {
+ padding: 1rem !important;
+ }
+ .p-xl-4 {
+ padding: 1.5rem !important;
+ }
+ .p-xl-5 {
+ padding: 3rem !important;
+ }
+ .px-xl-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+ .px-xl-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+ }
+ .px-xl-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+ }
+ .px-xl-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+ }
+ .px-xl-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+ }
+ .px-xl-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+ }
+ .py-xl-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ }
+ .py-xl-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+ .py-xl-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+ .py-xl-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+ }
+ .py-xl-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+ }
+ .py-xl-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+ }
+ .pt-xl-0 {
+ padding-top: 0 !important;
+ }
+ .pt-xl-1 {
+ padding-top: 0.25rem !important;
+ }
+ .pt-xl-2 {
+ padding-top: 0.5rem !important;
+ }
+ .pt-xl-3 {
+ padding-top: 1rem !important;
+ }
+ .pt-xl-4 {
+ padding-top: 1.5rem !important;
+ }
+ .pt-xl-5 {
+ padding-top: 3rem !important;
+ }
+ .pe-xl-0 {
+ padding-right: 0 !important;
+ }
+ .pe-xl-1 {
+ padding-right: 0.25rem !important;
+ }
+ .pe-xl-2 {
+ padding-right: 0.5rem !important;
+ }
+ .pe-xl-3 {
+ padding-right: 1rem !important;
+ }
+ .pe-xl-4 {
+ padding-right: 1.5rem !important;
+ }
+ .pe-xl-5 {
+ padding-right: 3rem !important;
+ }
+ .pb-xl-0 {
+ padding-bottom: 0 !important;
+ }
+ .pb-xl-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ .pb-xl-2 {
+ padding-bottom: 0.5rem !important;
+ }
+ .pb-xl-3 {
+ padding-bottom: 1rem !important;
+ }
+ .pb-xl-4 {
+ padding-bottom: 1.5rem !important;
+ }
+ .pb-xl-5 {
+ padding-bottom: 3rem !important;
+ }
+ .ps-xl-0 {
+ padding-left: 0 !important;
+ }
+ .ps-xl-1 {
+ padding-left: 0.25rem !important;
+ }
+ .ps-xl-2 {
+ padding-left: 0.5rem !important;
+ }
+ .ps-xl-3 {
+ padding-left: 1rem !important;
+ }
+ .ps-xl-4 {
+ padding-left: 1.5rem !important;
+ }
+ .ps-xl-5 {
+ padding-left: 3rem !important;
+ }
+ .gap-xl-0 {
+ gap: 0 !important;
+ }
+ .gap-xl-1 {
+ gap: 0.25rem !important;
+ }
+ .gap-xl-2 {
+ gap: 0.5rem !important;
+ }
+ .gap-xl-3 {
+ gap: 1rem !important;
+ }
+ .gap-xl-4 {
+ gap: 1.5rem !important;
+ }
+ .gap-xl-5 {
+ gap: 3rem !important;
+ }
+ .row-gap-xl-0 {
+ row-gap: 0 !important;
+ }
+ .row-gap-xl-1 {
+ row-gap: 0.25rem !important;
+ }
+ .row-gap-xl-2 {
+ row-gap: 0.5rem !important;
+ }
+ .row-gap-xl-3 {
+ row-gap: 1rem !important;
+ }
+ .row-gap-xl-4 {
+ row-gap: 1.5rem !important;
+ }
+ .row-gap-xl-5 {
+ row-gap: 3rem !important;
+ }
+ .column-gap-xl-0 {
+ -moz-column-gap: 0 !important;
+ column-gap: 0 !important;
+ }
+ .column-gap-xl-1 {
+ -moz-column-gap: 0.25rem !important;
+ column-gap: 0.25rem !important;
+ }
+ .column-gap-xl-2 {
+ -moz-column-gap: 0.5rem !important;
+ column-gap: 0.5rem !important;
+ }
+ .column-gap-xl-3 {
+ -moz-column-gap: 1rem !important;
+ column-gap: 1rem !important;
+ }
+ .column-gap-xl-4 {
+ -moz-column-gap: 1.5rem !important;
+ column-gap: 1.5rem !important;
+ }
+ .column-gap-xl-5 {
+ -moz-column-gap: 3rem !important;
+ column-gap: 3rem !important;
+ }
+ .text-xl-start {
+ text-align: left !important;
+ }
+ .text-xl-end {
+ text-align: right !important;
+ }
+ .text-xl-center {
+ text-align: center !important;
+ }
+}
+@media (min-width: 1400px) {
+ .float-xxl-start {
+ float: left !important;
+ }
+ .float-xxl-end {
+ float: right !important;
+ }
+ .float-xxl-none {
+ float: none !important;
+ }
+ .object-fit-xxl-contain {
+ -o-object-fit: contain !important;
+ object-fit: contain !important;
+ }
+ .object-fit-xxl-cover {
+ -o-object-fit: cover !important;
+ object-fit: cover !important;
+ }
+ .object-fit-xxl-fill {
+ -o-object-fit: fill !important;
+ object-fit: fill !important;
+ }
+ .object-fit-xxl-scale {
+ -o-object-fit: scale-down !important;
+ object-fit: scale-down !important;
+ }
+ .object-fit-xxl-none {
+ -o-object-fit: none !important;
+ object-fit: none !important;
+ }
+ .d-xxl-inline {
+ display: inline !important;
+ }
+ .d-xxl-inline-block {
+ display: inline-block !important;
+ }
+ .d-xxl-block {
+ display: block !important;
+ }
+ .d-xxl-grid {
+ display: grid !important;
+ }
+ .d-xxl-inline-grid {
+ display: inline-grid !important;
+ }
+ .d-xxl-table {
+ display: table !important;
+ }
+ .d-xxl-table-row {
+ display: table-row !important;
+ }
+ .d-xxl-table-cell {
+ display: table-cell !important;
+ }
+ .d-xxl-flex {
+ display: flex !important;
+ }
+ .d-xxl-inline-flex {
+ display: inline-flex !important;
+ }
+ .d-xxl-none {
+ display: none !important;
+ }
+ .flex-xxl-fill {
+ flex: 1 1 auto !important;
+ }
+ .flex-xxl-row {
+ flex-direction: row !important;
+ }
+ .flex-xxl-column {
+ flex-direction: column !important;
+ }
+ .flex-xxl-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+ .flex-xxl-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+ .flex-xxl-grow-0 {
+ flex-grow: 0 !important;
+ }
+ .flex-xxl-grow-1 {
+ flex-grow: 1 !important;
+ }
+ .flex-xxl-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+ .flex-xxl-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+ .flex-xxl-wrap {
+ flex-wrap: wrap !important;
+ }
+ .flex-xxl-nowrap {
+ flex-wrap: nowrap !important;
+ }
+ .flex-xxl-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+ .justify-content-xxl-start {
+ justify-content: flex-start !important;
+ }
+ .justify-content-xxl-end {
+ justify-content: flex-end !important;
+ }
+ .justify-content-xxl-center {
+ justify-content: center !important;
+ }
+ .justify-content-xxl-between {
+ justify-content: space-between !important;
+ }
+ .justify-content-xxl-around {
+ justify-content: space-around !important;
+ }
+ .justify-content-xxl-evenly {
+ justify-content: space-evenly !important;
+ }
+ .align-items-xxl-start {
+ align-items: flex-start !important;
+ }
+ .align-items-xxl-end {
+ align-items: flex-end !important;
+ }
+ .align-items-xxl-center {
+ align-items: center !important;
+ }
+ .align-items-xxl-baseline {
+ align-items: baseline !important;
+ }
+ .align-items-xxl-stretch {
+ align-items: stretch !important;
+ }
+ .align-content-xxl-start {
+ align-content: flex-start !important;
+ }
+ .align-content-xxl-end {
+ align-content: flex-end !important;
+ }
+ .align-content-xxl-center {
+ align-content: center !important;
+ }
+ .align-content-xxl-between {
+ align-content: space-between !important;
+ }
+ .align-content-xxl-around {
+ align-content: space-around !important;
+ }
+ .align-content-xxl-stretch {
+ align-content: stretch !important;
+ }
+ .align-self-xxl-auto {
+ align-self: auto !important;
+ }
+ .align-self-xxl-start {
+ align-self: flex-start !important;
+ }
+ .align-self-xxl-end {
+ align-self: flex-end !important;
+ }
+ .align-self-xxl-center {
+ align-self: center !important;
+ }
+ .align-self-xxl-baseline {
+ align-self: baseline !important;
+ }
+ .align-self-xxl-stretch {
+ align-self: stretch !important;
+ }
+ .order-xxl-first {
+ order: -1 !important;
+ }
+ .order-xxl-0 {
+ order: 0 !important;
+ }
+ .order-xxl-1 {
+ order: 1 !important;
+ }
+ .order-xxl-2 {
+ order: 2 !important;
+ }
+ .order-xxl-3 {
+ order: 3 !important;
+ }
+ .order-xxl-4 {
+ order: 4 !important;
+ }
+ .order-xxl-5 {
+ order: 5 !important;
+ }
+ .order-xxl-last {
+ order: 6 !important;
+ }
+ .m-xxl-0 {
+ margin: 0 !important;
+ }
+ .m-xxl-1 {
+ margin: 0.25rem !important;
+ }
+ .m-xxl-2 {
+ margin: 0.5rem !important;
+ }
+ .m-xxl-3 {
+ margin: 1rem !important;
+ }
+ .m-xxl-4 {
+ margin: 1.5rem !important;
+ }
+ .m-xxl-5 {
+ margin: 3rem !important;
+ }
+ .m-xxl-auto {
+ margin: auto !important;
+ }
+ .mx-xxl-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
+ .mx-xxl-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+ }
+ .mx-xxl-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+ }
+ .mx-xxl-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+ }
+ .mx-xxl-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+ }
+ .mx-xxl-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+ }
+ .mx-xxl-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+ }
+ .my-xxl-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+ .my-xxl-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+ }
+ .my-xxl-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+ .my-xxl-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+ }
+ .my-xxl-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+ }
+ .my-xxl-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+ }
+ .my-xxl-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ }
+ .mt-xxl-0 {
+ margin-top: 0 !important;
+ }
+ .mt-xxl-1 {
+ margin-top: 0.25rem !important;
+ }
+ .mt-xxl-2 {
+ margin-top: 0.5rem !important;
+ }
+ .mt-xxl-3 {
+ margin-top: 1rem !important;
+ }
+ .mt-xxl-4 {
+ margin-top: 1.5rem !important;
+ }
+ .mt-xxl-5 {
+ margin-top: 3rem !important;
+ }
+ .mt-xxl-auto {
+ margin-top: auto !important;
+ }
+ .me-xxl-0 {
+ margin-right: 0 !important;
+ }
+ .me-xxl-1 {
+ margin-right: 0.25rem !important;
+ }
+ .me-xxl-2 {
+ margin-right: 0.5rem !important;
+ }
+ .me-xxl-3 {
+ margin-right: 1rem !important;
+ }
+ .me-xxl-4 {
+ margin-right: 1.5rem !important;
+ }
+ .me-xxl-5 {
+ margin-right: 3rem !important;
+ }
+ .me-xxl-auto {
+ margin-right: auto !important;
+ }
+ .mb-xxl-0 {
+ margin-bottom: 0 !important;
+ }
+ .mb-xxl-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ .mb-xxl-2 {
+ margin-bottom: 0.5rem !important;
+ }
+ .mb-xxl-3 {
+ margin-bottom: 1rem !important;
+ }
+ .mb-xxl-4 {
+ margin-bottom: 1.5rem !important;
+ }
+ .mb-xxl-5 {
+ margin-bottom: 3rem !important;
+ }
+ .mb-xxl-auto {
+ margin-bottom: auto !important;
+ }
+ .ms-xxl-0 {
+ margin-left: 0 !important;
+ }
+ .ms-xxl-1 {
+ margin-left: 0.25rem !important;
+ }
+ .ms-xxl-2 {
+ margin-left: 0.5rem !important;
+ }
+ .ms-xxl-3 {
+ margin-left: 1rem !important;
+ }
+ .ms-xxl-4 {
+ margin-left: 1.5rem !important;
+ }
+ .ms-xxl-5 {
+ margin-left: 3rem !important;
+ }
+ .ms-xxl-auto {
+ margin-left: auto !important;
+ }
+ .p-xxl-0 {
+ padding: 0 !important;
+ }
+ .p-xxl-1 {
+ padding: 0.25rem !important;
+ }
+ .p-xxl-2 {
+ padding: 0.5rem !important;
+ }
+ .p-xxl-3 {
+ padding: 1rem !important;
+ }
+ .p-xxl-4 {
+ padding: 1.5rem !important;
+ }
+ .p-xxl-5 {
+ padding: 3rem !important;
+ }
+ .px-xxl-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+ .px-xxl-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+ }
+ .px-xxl-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+ }
+ .px-xxl-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+ }
+ .px-xxl-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+ }
+ .px-xxl-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+ }
+ .py-xxl-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ }
+ .py-xxl-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+ .py-xxl-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+ .py-xxl-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+ }
+ .py-xxl-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+ }
+ .py-xxl-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+ }
+ .pt-xxl-0 {
+ padding-top: 0 !important;
+ }
+ .pt-xxl-1 {
+ padding-top: 0.25rem !important;
+ }
+ .pt-xxl-2 {
+ padding-top: 0.5rem !important;
+ }
+ .pt-xxl-3 {
+ padding-top: 1rem !important;
+ }
+ .pt-xxl-4 {
+ padding-top: 1.5rem !important;
+ }
+ .pt-xxl-5 {
+ padding-top: 3rem !important;
+ }
+ .pe-xxl-0 {
+ padding-right: 0 !important;
+ }
+ .pe-xxl-1 {
+ padding-right: 0.25rem !important;
+ }
+ .pe-xxl-2 {
+ padding-right: 0.5rem !important;
+ }
+ .pe-xxl-3 {
+ padding-right: 1rem !important;
+ }
+ .pe-xxl-4 {
+ padding-right: 1.5rem !important;
+ }
+ .pe-xxl-5 {
+ padding-right: 3rem !important;
+ }
+ .pb-xxl-0 {
+ padding-bottom: 0 !important;
+ }
+ .pb-xxl-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ .pb-xxl-2 {
+ padding-bottom: 0.5rem !important;
+ }
+ .pb-xxl-3 {
+ padding-bottom: 1rem !important;
+ }
+ .pb-xxl-4 {
+ padding-bottom: 1.5rem !important;
+ }
+ .pb-xxl-5 {
+ padding-bottom: 3rem !important;
+ }
+ .ps-xxl-0 {
+ padding-left: 0 !important;
+ }
+ .ps-xxl-1 {
+ padding-left: 0.25rem !important;
+ }
+ .ps-xxl-2 {
+ padding-left: 0.5rem !important;
+ }
+ .ps-xxl-3 {
+ padding-left: 1rem !important;
+ }
+ .ps-xxl-4 {
+ padding-left: 1.5rem !important;
+ }
+ .ps-xxl-5 {
+ padding-left: 3rem !important;
+ }
+ .gap-xxl-0 {
+ gap: 0 !important;
+ }
+ .gap-xxl-1 {
+ gap: 0.25rem !important;
+ }
+ .gap-xxl-2 {
+ gap: 0.5rem !important;
+ }
+ .gap-xxl-3 {
+ gap: 1rem !important;
+ }
+ .gap-xxl-4 {
+ gap: 1.5rem !important;
+ }
+ .gap-xxl-5 {
+ gap: 3rem !important;
+ }
+ .row-gap-xxl-0 {
+ row-gap: 0 !important;
+ }
+ .row-gap-xxl-1 {
+ row-gap: 0.25rem !important;
+ }
+ .row-gap-xxl-2 {
+ row-gap: 0.5rem !important;
+ }
+ .row-gap-xxl-3 {
+ row-gap: 1rem !important;
+ }
+ .row-gap-xxl-4 {
+ row-gap: 1.5rem !important;
+ }
+ .row-gap-xxl-5 {
+ row-gap: 3rem !important;
+ }
+ .column-gap-xxl-0 {
+ -moz-column-gap: 0 !important;
+ column-gap: 0 !important;
+ }
+ .column-gap-xxl-1 {
+ -moz-column-gap: 0.25rem !important;
+ column-gap: 0.25rem !important;
+ }
+ .column-gap-xxl-2 {
+ -moz-column-gap: 0.5rem !important;
+ column-gap: 0.5rem !important;
+ }
+ .column-gap-xxl-3 {
+ -moz-column-gap: 1rem !important;
+ column-gap: 1rem !important;
+ }
+ .column-gap-xxl-4 {
+ -moz-column-gap: 1.5rem !important;
+ column-gap: 1.5rem !important;
+ }
+ .column-gap-xxl-5 {
+ -moz-column-gap: 3rem !important;
+ column-gap: 3rem !important;
+ }
+ .text-xxl-start {
+ text-align: left !important;
+ }
+ .text-xxl-end {
+ text-align: right !important;
+ }
+ .text-xxl-center {
+ text-align: center !important;
+ }
+}
+@media (min-width: 1200px) {
+ .fs-1 {
+ font-size: 2.5rem !important;
+ }
+ .fs-2 {
+ font-size: 2rem !important;
+ }
+ .fs-3 {
+ font-size: 1.75rem !important;
+ }
+ .fs-4 {
+ font-size: 1.5rem !important;
+ }
+}
+@media print {
+ .d-print-inline {
+ display: inline !important;
+ }
+ .d-print-inline-block {
+ display: inline-block !important;
+ }
+ .d-print-block {
+ display: block !important;
+ }
+ .d-print-grid {
+ display: grid !important;
+ }
+ .d-print-inline-grid {
+ display: inline-grid !important;
+ }
+ .d-print-table {
+ display: table !important;
+ }
+ .d-print-table-row {
+ display: table-row !important;
+ }
+ .d-print-table-cell {
+ display: table-cell !important;
+ }
+ .d-print-flex {
+ display: flex !important;
+ }
+ .d-print-inline-flex {
+ display: inline-flex !important;
+ }
+ .d-print-none {
+ display: none !important;
+ }
+}
+
+.container {
+ max-width: 960px;
+}
+.bd-placeholder-img {
+ font-size: 1.125rem;
+ text-anchor: middle;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+@media (min-width: 768px) {
+ .bd-placeholder-img-lg {
+ font-size: 3.5rem;
+ }
+}
+
+.b-example-divider {
+ width: 100%;
+ height: 3rem;
+ background-color: rgba(0, 0, 0, 0.1);
+ border: solid rgba(0, 0, 0, 0.15);
+ border-width: 1px 0;
+ box-shadow: inset 0 0.5em 1.5em rgba(0, 0, 0, 0.1),
+ inset 0 0.125em 0.5em rgba(0, 0, 0, 0.15);
+}
+
+.b-example-vr {
+ flex-shrink: 0;
+ width: 1.5rem;
+ height: 100vh;
+}
+
+.bi {
+ vertical-align: -0.125em;
+ fill: currentColor;
+}
+
+.nav-scroller {
+ position: relative;
+ z-index: 2;
+ height: 2.75rem;
+ overflow-y: hidden;
+}
+
+.nav-scroller .nav {
+ display: flex;
+ flex-wrap: nowrap;
+ padding-bottom: 1rem;
+ margin-top: -1px;
+ overflow-x: auto;
+ text-align: center;
+ white-space: nowrap;
+ -webkit-overflow-scrolling: touch;
+}
+
+.btn-bd-primary {
+ --bd-violet-bg: #712cf9;
+ --bd-violet-rgb: 112.520718, 44.062154, 249.437846;
+
+ --bs-btn-font-weight: 600;
+ --bs-btn-color: var(--bs-white);
+ --bs-btn-bg: var(--bd-violet-bg);
+ --bs-btn-border-color: var(--bd-violet-bg);
+ --bs-btn-hover-color: var(--bs-white);
+ --bs-btn-hover-bg: #6528e0;
+ --bs-btn-hover-border-color: #6528e0;
+ --bs-btn-focus-shadow-rgb: var(--bd-violet-rgb);
+ --bs-btn-active-color: var(--bs-btn-hover-color);
+ --bs-btn-active-bg: #5a23c8;
+ --bs-btn-active-border-color: #5a23c8;
+}
+
+.bd-mode-toggle {
+ z-index: 1500;
+}
+
+.bd-mode-toggle .dropdown-menu .active .bi {
+ display: block !important;
+}
\ No newline at end of file
diff --git a/data_embed/bootstrap.js b/data_embed/bootstrap.js
new file mode 100644
index 0000000..ded4eb2
--- /dev/null
+++ b/data_embed/bootstrap.js
@@ -0,0 +1,4467 @@
+// Bootstrap base
+/*!
+ * Bootstrap v5.3.2 (https://getbootstrap.com/)
+ * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/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).bootstrap = e());
+})(this, function () {
+ "use strict";
+ const t = new Map(),
+ e = {
+ set(e, i, n) {
+ t.has(e) || t.set(e, new Map());
+ const s = t.get(e);
+ s.has(i) || 0 === s.size
+ ? s.set(i, n)
+ : console.error(
+ `Bootstrap doesn't allow more than one instance per element. Bound instance: ${
+ Array.from(s.keys())[0]
+ }.`
+ );
+ },
+ get: (e, i) => (t.has(e) && t.get(e).get(i)) || null,
+ remove(e, i) {
+ if (!t.has(e)) return;
+ const n = t.get(e);
+ n.delete(i), 0 === n.size && t.delete(e);
+ },
+ },
+ i = "transitionend",
+ n = (t) => (
+ t &&
+ window.CSS &&
+ window.CSS.escape &&
+ (t = t.replace(/#([^\s"#']+)/g, (t, e) => `#${CSS.escape(e)}`)),
+ t
+ ),
+ s = (t) => {
+ t.dispatchEvent(new Event(i));
+ },
+ o = (t) =>
+ !(!t || "object" != typeof t) &&
+ (void 0 !== t.jquery && (t = t[0]), void 0 !== t.nodeType),
+ r = (t) =>
+ o(t)
+ ? t.jquery
+ ? t[0]
+ : t
+ : "string" == typeof t && t.length > 0
+ ? document.querySelector(n(t))
+ : null,
+ a = (t) => {
+ if (!o(t) || 0 === t.getClientRects().length) return !1;
+ const e =
+ "visible" ===
+ getComputedStyle(t).getPropertyValue("visibility"),
+ i = t.closest("details:not([open])");
+ if (!i) return e;
+ if (i !== t) {
+ const e = t.closest("summary");
+ if (e && e.parentNode !== i) return !1;
+ if (null === e) return !1;
+ }
+ return e;
+ },
+ l = (t) =>
+ !t ||
+ t.nodeType !== Node.ELEMENT_NODE ||
+ !!t.classList.contains("disabled") ||
+ (void 0 !== t.disabled
+ ? t.disabled
+ : t.hasAttribute("disabled") &&
+ "false" !== t.getAttribute("disabled")),
+ c = (t) => {
+ if (!document.documentElement.attachShadow) return null;
+ if ("function" == typeof t.getRootNode) {
+ const e = t.getRootNode();
+ return e instanceof ShadowRoot ? e : null;
+ }
+ return t instanceof ShadowRoot
+ ? t
+ : t.parentNode
+ ? c(t.parentNode)
+ : null;
+ },
+ h = () => {},
+ d = (t) => {
+ t.offsetHeight;
+ },
+ u = () =>
+ window.jQuery && !document.body.hasAttribute("data-bs-no-jquery")
+ ? window.jQuery
+ : null,
+ f = [],
+ p = () => "rtl" === document.documentElement.dir,
+ m = (t) => {
+ var e;
+ (e = () => {
+ const e = u();
+ if (e) {
+ const i = t.NAME,
+ n = e.fn[i];
+ (e.fn[i] = t.jQueryInterface),
+ (e.fn[i].Constructor = t),
+ (e.fn[i].noConflict = () => (
+ (e.fn[i] = n), t.jQueryInterface
+ ));
+ }
+ }),
+ "loading" === document.readyState
+ ? (f.length ||
+ document.addEventListener("DOMContentLoaded", () => {
+ for (const t of f) t();
+ }),
+ f.push(e))
+ : e();
+ },
+ g = (t, e = [], i = t) => ("function" == typeof t ? t(...e) : i),
+ _ = (t, e, n = !0) => {
+ if (!n) return void g(t);
+ const o =
+ ((t) => {
+ if (!t) return 0;
+ let { transitionDuration: e, transitionDelay: i } =
+ window.getComputedStyle(t);
+ const n = Number.parseFloat(e),
+ s = Number.parseFloat(i);
+ return n || s
+ ? ((e = e.split(",")[0]),
+ (i = i.split(",")[0]),
+ 1e3 * (Number.parseFloat(e) + Number.parseFloat(i)))
+ : 0;
+ })(e) + 5;
+ let r = !1;
+ const a = ({ target: n }) => {
+ n === e && ((r = !0), e.removeEventListener(i, a), g(t));
+ };
+ e.addEventListener(i, a),
+ setTimeout(() => {
+ r || s(e);
+ }, o);
+ },
+ b = (t, e, i, n) => {
+ const s = t.length;
+ let o = t.indexOf(e);
+ return -1 === o
+ ? !i && n
+ ? t[s - 1]
+ : t[0]
+ : ((o += i ? 1 : -1),
+ n && (o = (o + s) % s),
+ t[Math.max(0, Math.min(o, s - 1))]);
+ },
+ v = /[^.]*(?=\..*)\.|.*/,
+ y = /\..*/,
+ w = /::\d+$/,
+ A = {};
+ let E = 1;
+ const T = { mouseenter: "mouseover", mouseleave: "mouseout" },
+ C = new Set([
+ "click",
+ "dblclick",
+ "mouseup",
+ "mousedown",
+ "contextmenu",
+ "mousewheel",
+ "DOMMouseScroll",
+ "mouseover",
+ "mouseout",
+ "mousemove",
+ "selectstart",
+ "selectend",
+ "keydown",
+ "keypress",
+ "keyup",
+ "orientationchange",
+ "touchstart",
+ "touchmove",
+ "touchend",
+ "touchcancel",
+ "pointerdown",
+ "pointermove",
+ "pointerup",
+ "pointerleave",
+ "pointercancel",
+ "gesturestart",
+ "gesturechange",
+ "gestureend",
+ "focus",
+ "blur",
+ "change",
+ "reset",
+ "select",
+ "submit",
+ "focusin",
+ "focusout",
+ "load",
+ "unload",
+ "beforeunload",
+ "resize",
+ "move",
+ "DOMContentLoaded",
+ "readystatechange",
+ "error",
+ "abort",
+ "scroll",
+ ]);
+ function O(t, e) {
+ return (e && `${e}::${E++}`) || t.uidEvent || E++;
+ }
+ function x(t) {
+ const e = O(t);
+ return (t.uidEvent = e), (A[e] = A[e] || {}), A[e];
+ }
+ function k(t, e, i = null) {
+ return Object.values(t).find(
+ (t) => t.callable === e && t.delegationSelector === i
+ );
+ }
+ function L(t, e, i) {
+ const n = "string" == typeof e,
+ s = n ? i : e || i;
+ let o = I(t);
+ return C.has(o) || (o = t), [n, s, o];
+ }
+ function S(t, e, i, n, s) {
+ if ("string" != typeof e || !t) return;
+ let [o, r, a] = L(e, i, n);
+ if (e in T) {
+ const t = (t) =>
+ function (e) {
+ if (
+ !e.relatedTarget ||
+ (e.relatedTarget !== e.delegateTarget &&
+ !e.delegateTarget.contains(e.relatedTarget))
+ )
+ return t.call(this, e);
+ };
+ r = t(r);
+ }
+ const l = x(t),
+ c = l[a] || (l[a] = {}),
+ h = k(c, r, o ? i : null);
+ if (h) return void (h.oneOff = h.oneOff && s);
+ const d = O(r, e.replace(v, "")),
+ u = o
+ ? (function (t, e, i) {
+ return function n(s) {
+ const o = t.querySelectorAll(e);
+ for (
+ let { target: r } = s;
+ r && r !== this;
+ r = r.parentNode
+ )
+ for (const a of o)
+ if (a === r)
+ return (
+ P(s, { delegateTarget: r }),
+ n.oneOff && N.off(t, s.type, e, i),
+ i.apply(r, [s])
+ );
+ };
+ })(t, i, r)
+ : (function (t, e) {
+ return function i(n) {
+ return (
+ P(n, { delegateTarget: t }),
+ i.oneOff && N.off(t, n.type, e),
+ e.apply(t, [n])
+ );
+ };
+ })(t, r);
+ (u.delegationSelector = o ? i : null),
+ (u.callable = r),
+ (u.oneOff = s),
+ (u.uidEvent = d),
+ (c[d] = u),
+ t.addEventListener(a, u, o);
+ }
+ function D(t, e, i, n, s) {
+ const o = k(e[i], n, s);
+ o && (t.removeEventListener(i, o, Boolean(s)), delete e[i][o.uidEvent]);
+ }
+ function $(t, e, i, n) {
+ const s = e[i] || {};
+ for (const [o, r] of Object.entries(s))
+ o.includes(n) && D(t, e, i, r.callable, r.delegationSelector);
+ }
+ function I(t) {
+ return (t = t.replace(y, "")), T[t] || t;
+ }
+ const N = {
+ on(t, e, i, n) {
+ S(t, e, i, n, !1);
+ },
+ one(t, e, i, n) {
+ S(t, e, i, n, !0);
+ },
+ off(t, e, i, n) {
+ if ("string" != typeof e || !t) return;
+ const [s, o, r] = L(e, i, n),
+ a = r !== e,
+ l = x(t),
+ c = l[r] || {},
+ h = e.startsWith(".");
+ if (void 0 === o) {
+ if (h) for (const i of Object.keys(l)) $(t, l, i, e.slice(1));
+ for (const [i, n] of Object.entries(c)) {
+ const s = i.replace(w, "");
+ (a && !e.includes(s)) ||
+ D(t, l, r, n.callable, n.delegationSelector);
+ }
+ } else {
+ if (!Object.keys(c).length) return;
+ D(t, l, r, o, s ? i : null);
+ }
+ },
+ trigger(t, e, i) {
+ if ("string" != typeof e || !t) return null;
+ const n = u();
+ let s = null,
+ o = !0,
+ r = !0,
+ a = !1;
+ e !== I(e) &&
+ n &&
+ ((s = n.Event(e, i)),
+ n(t).trigger(s),
+ (o = !s.isPropagationStopped()),
+ (r = !s.isImmediatePropagationStopped()),
+ (a = s.isDefaultPrevented()));
+ const l = P(new Event(e, { bubbles: o, cancelable: !0 }), i);
+ return (
+ a && l.preventDefault(),
+ r && t.dispatchEvent(l),
+ l.defaultPrevented && s && s.preventDefault(),
+ l
+ );
+ },
+ };
+ function P(t, e = {}) {
+ for (const [i, n] of Object.entries(e))
+ try {
+ t[i] = n;
+ } catch (e) {
+ Object.defineProperty(t, i, { configurable: !0, get: () => n });
+ }
+ return t;
+ }
+ function M(t) {
+ if ("true" === t) return !0;
+ if ("false" === t) return !1;
+ if (t === Number(t).toString()) return Number(t);
+ if ("" === t || "null" === t) return null;
+ if ("string" != typeof t) return t;
+ try {
+ return JSON.parse(decodeURIComponent(t));
+ } catch (e) {
+ return t;
+ }
+ }
+ function j(t) {
+ return t.replace(/[A-Z]/g, (t) => `-${t.toLowerCase()}`);
+ }
+ const F = {
+ setDataAttribute(t, e, i) {
+ t.setAttribute(`data-bs-${j(e)}`, i);
+ },
+ removeDataAttribute(t, e) {
+ t.removeAttribute(`data-bs-${j(e)}`);
+ },
+ getDataAttributes(t) {
+ if (!t) return {};
+ const e = {},
+ i = Object.keys(t.dataset).filter(
+ (t) => t.startsWith("bs") && !t.startsWith("bsConfig")
+ );
+ for (const n of i) {
+ let i = n.replace(/^bs/, "");
+ (i = i.charAt(0).toLowerCase() + i.slice(1, i.length)),
+ (e[i] = M(t.dataset[n]));
+ }
+ return e;
+ },
+ getDataAttribute: (t, e) => M(t.getAttribute(`data-bs-${j(e)}`)),
+ };
+ class H {
+ static get Default() {
+ return {};
+ }
+ static get DefaultType() {
+ return {};
+ }
+ static get NAME() {
+ throw new Error(
+ 'You have to implement the static method "NAME", for each component!'
+ );
+ }
+ _getConfig(t) {
+ return (
+ (t = this._mergeConfigObj(t)),
+ (t = this._configAfterMerge(t)),
+ this._typeCheckConfig(t),
+ t
+ );
+ }
+ _configAfterMerge(t) {
+ return t;
+ }
+ _mergeConfigObj(t, e) {
+ const i = o(e) ? F.getDataAttribute(e, "config") : {};
+ return {
+ ...this.constructor.Default,
+ ...("object" == typeof i ? i : {}),
+ ...(o(e) ? F.getDataAttributes(e) : {}),
+ ...("object" == typeof t ? t : {}),
+ };
+ }
+ _typeCheckConfig(t, e = this.constructor.DefaultType) {
+ for (const [n, s] of Object.entries(e)) {
+ const e = t[n],
+ r = o(e)
+ ? "element"
+ : null == (i = e)
+ ? `${i}`
+ : Object.prototype.toString
+ .call(i)
+ .match(/\s([a-z]+)/i)[1]
+ .toLowerCase();
+ if (!new RegExp(s).test(r))
+ throw new TypeError(
+ `${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`
+ );
+ }
+ var i;
+ }
+ }
+ class W extends H {
+ constructor(t, i) {
+ super(),
+ (t = r(t)) &&
+ ((this._element = t),
+ (this._config = this._getConfig(i)),
+ e.set(this._element, this.constructor.DATA_KEY, this));
+ }
+ dispose() {
+ e.remove(this._element, this.constructor.DATA_KEY),
+ N.off(this._element, this.constructor.EVENT_KEY);
+ for (const t of Object.getOwnPropertyNames(this)) this[t] = null;
+ }
+ _queueCallback(t, e, i = !0) {
+ _(t, e, i);
+ }
+ _getConfig(t) {
+ return (
+ (t = this._mergeConfigObj(t, this._element)),
+ (t = this._configAfterMerge(t)),
+ this._typeCheckConfig(t),
+ t
+ );
+ }
+ static getInstance(t) {
+ return e.get(r(t), this.DATA_KEY);
+ }
+ static getOrCreateInstance(t, e = {}) {
+ return (
+ this.getInstance(t) ||
+ new this(t, "object" == typeof e ? e : null)
+ );
+ }
+ static get VERSION() {
+ return "5.3.2";
+ }
+ static get DATA_KEY() {
+ return `bs.${this.NAME}`;
+ }
+ static get EVENT_KEY() {
+ return `.${this.DATA_KEY}`;
+ }
+ static eventName(t) {
+ return `${t}${this.EVENT_KEY}`;
+ }
+ }
+ const B = (t) => {
+ let e = t.getAttribute("data-bs-target");
+ if (!e || "#" === e) {
+ let i = t.getAttribute("href");
+ if (!i || (!i.includes("#") && !i.startsWith("."))) return null;
+ i.includes("#") &&
+ !i.startsWith("#") &&
+ (i = `#${i.split("#")[1]}`),
+ (e = i && "#" !== i ? n(i.trim()) : null);
+ }
+ return e;
+ },
+ z = {
+ find: (t, e = document.documentElement) =>
+ [].concat(...Element.prototype.querySelectorAll.call(e, t)),
+ findOne: (t, e = document.documentElement) =>
+ Element.prototype.querySelector.call(e, t),
+ children: (t, e) =>
+ [].concat(...t.children).filter((t) => t.matches(e)),
+ parents(t, e) {
+ const i = [];
+ let n = t.parentNode.closest(e);
+ for (; n; ) i.push(n), (n = n.parentNode.closest(e));
+ return i;
+ },
+ prev(t, e) {
+ let i = t.previousElementSibling;
+ for (; i; ) {
+ if (i.matches(e)) return [i];
+ i = i.previousElementSibling;
+ }
+ return [];
+ },
+ next(t, e) {
+ let i = t.nextElementSibling;
+ for (; i; ) {
+ if (i.matches(e)) return [i];
+ i = i.nextElementSibling;
+ }
+ return [];
+ },
+ focusableChildren(t) {
+ const e = [
+ "a",
+ "button",
+ "input",
+ "textarea",
+ "select",
+ "details",
+ "[tabindex]",
+ '[contenteditable="true"]',
+ ]
+ .map((t) => `${t}:not([tabindex^="-"])`)
+ .join(",");
+ return this.find(e, t).filter((t) => !l(t) && a(t));
+ },
+ getSelectorFromElement(t) {
+ const e = B(t);
+ return e && z.findOne(e) ? e : null;
+ },
+ getElementFromSelector(t) {
+ const e = B(t);
+ return e ? z.findOne(e) : null;
+ },
+ getMultipleElementsFromSelector(t) {
+ const e = B(t);
+ return e ? z.find(e) : [];
+ },
+ },
+ R = (t, e = "hide") => {
+ const i = `click.dismiss${t.EVENT_KEY}`,
+ n = t.NAME;
+ N.on(document, i, `[data-bs-dismiss="${n}"]`, function (i) {
+ if (
+ (["A", "AREA"].includes(this.tagName) && i.preventDefault(),
+ l(this))
+ )
+ return;
+ const s =
+ z.getElementFromSelector(this) || this.closest(`.${n}`);
+ t.getOrCreateInstance(s)[e]();
+ });
+ },
+ q = ".bs.alert",
+ V = `close${q}`,
+ K = `closed${q}`;
+ class Q extends W {
+ static get NAME() {
+ return "alert";
+ }
+ close() {
+ if (N.trigger(this._element, V).defaultPrevented) return;
+ this._element.classList.remove("show");
+ const t = this._element.classList.contains("fade");
+ this._queueCallback(() => this._destroyElement(), this._element, t);
+ }
+ _destroyElement() {
+ this._element.remove(), N.trigger(this._element, K), this.dispose();
+ }
+ static jQueryInterface(t) {
+ return this.each(function () {
+ const e = Q.getOrCreateInstance(this);
+ if ("string" == typeof t) {
+ if (
+ void 0 === e[t] ||
+ t.startsWith("_") ||
+ "constructor" === t
+ )
+ throw new TypeError(`No method named "${t}"`);
+ e[t](this);
+ }
+ });
+ }
+ }
+ R(Q, "close"), m(Q);
+ const X = '[data-bs-toggle="button"]';
+ class Y extends W {
+ static get NAME() {
+ return "button";
+ }
+ toggle() {
+ this._element.setAttribute(
+ "aria-pressed",
+ this._element.classList.toggle("active")
+ );
+ }
+ static jQueryInterface(t) {
+ return this.each(function () {
+ const e = Y.getOrCreateInstance(this);
+ "toggle" === t && e[t]();
+ });
+ }
+ }
+ N.on(document, "click.bs.button.data-api", X, (t) => {
+ t.preventDefault();
+ const e = t.target.closest(X);
+ Y.getOrCreateInstance(e).toggle();
+ }),
+ m(Y);
+ const U = ".bs.swipe",
+ G = `touchstart${U}`,
+ J = `touchmove${U}`,
+ Z = `touchend${U}`,
+ tt = `pointerdown${U}`,
+ et = `pointerup${U}`,
+ it = { endCallback: null, leftCallback: null, rightCallback: null },
+ nt = {
+ endCallback: "(function|null)",
+ leftCallback: "(function|null)",
+ rightCallback: "(function|null)",
+ };
+ class st extends H {
+ constructor(t, e) {
+ super(),
+ (this._element = t),
+ t &&
+ st.isSupported() &&
+ ((this._config = this._getConfig(e)),
+ (this._deltaX = 0),
+ (this._supportPointerEvents = Boolean(window.PointerEvent)),
+ this._initEvents());
+ }
+ static get Default() {
+ return it;
+ }
+ static get DefaultType() {
+ return nt;
+ }
+ static get NAME() {
+ return "swipe";
+ }
+ dispose() {
+ N.off(this._element, U);
+ }
+ _start(t) {
+ this._supportPointerEvents
+ ? this._eventIsPointerPenTouch(t) && (this._deltaX = t.clientX)
+ : (this._deltaX = t.touches[0].clientX);
+ }
+ _end(t) {
+ this._eventIsPointerPenTouch(t) &&
+ (this._deltaX = t.clientX - this._deltaX),
+ this._handleSwipe(),
+ g(this._config.endCallback);
+ }
+ _move(t) {
+ this._deltaX =
+ t.touches && t.touches.length > 1
+ ? 0
+ : t.touches[0].clientX - this._deltaX;
+ }
+ _handleSwipe() {
+ const t = Math.abs(this._deltaX);
+ if (t <= 40) return;
+ const e = t / this._deltaX;
+ (this._deltaX = 0),
+ e &&
+ g(
+ e > 0
+ ? this._config.rightCallback
+ : this._config.leftCallback
+ );
+ }
+ _initEvents() {
+ this._supportPointerEvents
+ ? (N.on(this._element, tt, (t) => this._start(t)),
+ N.on(this._element, et, (t) => this._end(t)),
+ this._element.classList.add("pointer-event"))
+ : (N.on(this._element, G, (t) => this._start(t)),
+ N.on(this._element, J, (t) => this._move(t)),
+ N.on(this._element, Z, (t) => this._end(t)));
+ }
+ _eventIsPointerPenTouch(t) {
+ return (
+ this._supportPointerEvents &&
+ ("pen" === t.pointerType || "touch" === t.pointerType)
+ );
+ }
+ static isSupported() {
+ return (
+ "ontouchstart" in document.documentElement ||
+ navigator.maxTouchPoints > 0
+ );
+ }
+ }
+ const ot = ".bs.carousel",
+ rt = ".data-api",
+ at = "next",
+ lt = "prev",
+ ct = "left",
+ ht = "right",
+ dt = `slide${ot}`,
+ ut = `slid${ot}`,
+ ft = `keydown${ot}`,
+ pt = `mouseenter${ot}`,
+ mt = `mouseleave${ot}`,
+ gt = `dragstart${ot}`,
+ _t = `load${ot}${rt}`,
+ bt = `click${ot}${rt}`,
+ vt = "carousel",
+ yt = "active",
+ wt = ".active",
+ At = ".carousel-item",
+ Et = wt + At,
+ Tt = { ArrowLeft: ht, ArrowRight: ct },
+ Ct = {
+ interval: 5e3,
+ keyboard: !0,
+ pause: "hover",
+ ride: !1,
+ touch: !0,
+ wrap: !0,
+ },
+ Ot = {
+ interval: "(number|boolean)",
+ keyboard: "boolean",
+ pause: "(string|boolean)",
+ ride: "(boolean|string)",
+ touch: "boolean",
+ wrap: "boolean",
+ };
+ class xt extends W {
+ constructor(t, e) {
+ super(t, e),
+ (this._interval = null),
+ (this._activeElement = null),
+ (this._isSliding = !1),
+ (this.touchTimeout = null),
+ (this._swipeHelper = null),
+ (this._indicatorsElement = z.findOne(
+ ".carousel-indicators",
+ this._element
+ )),
+ this._addEventListeners(),
+ this._config.ride === vt && this.cycle();
+ }
+ static get Default() {
+ return Ct;
+ }
+ static get DefaultType() {
+ return Ot;
+ }
+ static get NAME() {
+ return "carousel";
+ }
+ next() {
+ this._slide(at);
+ }
+ nextWhenVisible() {
+ !document.hidden && a(this._element) && this.next();
+ }
+ prev() {
+ this._slide(lt);
+ }
+ pause() {
+ this._isSliding && s(this._element), this._clearInterval();
+ }
+ cycle() {
+ this._clearInterval(),
+ this._updateInterval(),
+ (this._interval = setInterval(
+ () => this.nextWhenVisible(),
+ this._config.interval
+ ));
+ }
+ _maybeEnableCycle() {
+ this._config.ride &&
+ (this._isSliding
+ ? N.one(this._element, ut, () => this.cycle())
+ : this.cycle());
+ }
+ to(t) {
+ const e = this._getItems();
+ if (t > e.length - 1 || t < 0) return;
+ if (this._isSliding)
+ return void N.one(this._element, ut, () => this.to(t));
+ const i = this._getItemIndex(this._getActive());
+ if (i === t) return;
+ const n = t > i ? at : lt;
+ this._slide(n, e[t]);
+ }
+ dispose() {
+ this._swipeHelper && this._swipeHelper.dispose(), super.dispose();
+ }
+ _configAfterMerge(t) {
+ return (t.defaultInterval = t.interval), t;
+ }
+ _addEventListeners() {
+ this._config.keyboard &&
+ N.on(this._element, ft, (t) => this._keydown(t)),
+ "hover" === this._config.pause &&
+ (N.on(this._element, pt, () => this.pause()),
+ N.on(this._element, mt, () => this._maybeEnableCycle())),
+ this._config.touch &&
+ st.isSupported() &&
+ this._addTouchEventListeners();
+ }
+ _addTouchEventListeners() {
+ for (const t of z.find(".carousel-item img", this._element))
+ N.on(t, gt, (t) => t.preventDefault());
+ const t = {
+ leftCallback: () => this._slide(this._directionToOrder(ct)),
+ rightCallback: () => this._slide(this._directionToOrder(ht)),
+ endCallback: () => {
+ "hover" === this._config.pause &&
+ (this.pause(),
+ this.touchTimeout && clearTimeout(this.touchTimeout),
+ (this.touchTimeout = setTimeout(
+ () => this._maybeEnableCycle(),
+ 500 + this._config.interval
+ )));
+ },
+ };
+ this._swipeHelper = new st(this._element, t);
+ }
+ _keydown(t) {
+ if (/input|textarea/i.test(t.target.tagName)) return;
+ const e = Tt[t.key];
+ e && (t.preventDefault(), this._slide(this._directionToOrder(e)));
+ }
+ _getItemIndex(t) {
+ return this._getItems().indexOf(t);
+ }
+ _setActiveIndicatorElement(t) {
+ if (!this._indicatorsElement) return;
+ const e = z.findOne(wt, this._indicatorsElement);
+ e.classList.remove(yt), e.removeAttribute("aria-current");
+ const i = z.findOne(
+ `[data-bs-slide-to="${t}"]`,
+ this._indicatorsElement
+ );
+ i && (i.classList.add(yt), i.setAttribute("aria-current", "true"));
+ }
+ _updateInterval() {
+ const t = this._activeElement || this._getActive();
+ if (!t) return;
+ const e = Number.parseInt(t.getAttribute("data-bs-interval"), 10);
+ this._config.interval = e || this._config.defaultInterval;
+ }
+ _slide(t, e = null) {
+ if (this._isSliding) return;
+ const i = this._getActive(),
+ n = t === at,
+ s = e || b(this._getItems(), i, n, this._config.wrap);
+ if (s === i) return;
+ const o = this._getItemIndex(s),
+ r = (e) =>
+ N.trigger(this._element, e, {
+ relatedTarget: s,
+ direction: this._orderToDirection(t),
+ from: this._getItemIndex(i),
+ to: o,
+ });
+ if (r(dt).defaultPrevented) return;
+ if (!i || !s) return;
+ const a = Boolean(this._interval);
+ this.pause(),
+ (this._isSliding = !0),
+ this._setActiveIndicatorElement(o),
+ (this._activeElement = s);
+ const l = n ? "carousel-item-start" : "carousel-item-end",
+ c = n ? "carousel-item-next" : "carousel-item-prev";
+ s.classList.add(c),
+ d(s),
+ i.classList.add(l),
+ s.classList.add(l),
+ this._queueCallback(
+ () => {
+ s.classList.remove(l, c),
+ s.classList.add(yt),
+ i.classList.remove(yt, c, l),
+ (this._isSliding = !1),
+ r(ut);
+ },
+ i,
+ this._isAnimated()
+ ),
+ a && this.cycle();
+ }
+ _isAnimated() {
+ return this._element.classList.contains("slide");
+ }
+ _getActive() {
+ return z.findOne(Et, this._element);
+ }
+ _getItems() {
+ return z.find(At, this._element);
+ }
+ _clearInterval() {
+ this._interval &&
+ (clearInterval(this._interval), (this._interval = null));
+ }
+ _directionToOrder(t) {
+ return p() ? (t === ct ? lt : at) : t === ct ? at : lt;
+ }
+ _orderToDirection(t) {
+ return p() ? (t === lt ? ct : ht) : t === lt ? ht : ct;
+ }
+ static jQueryInterface(t) {
+ return this.each(function () {
+ const e = xt.getOrCreateInstance(this, t);
+ if ("number" != typeof t) {
+ if ("string" == typeof t) {
+ if (
+ void 0 === e[t] ||
+ t.startsWith("_") ||
+ "constructor" === t
+ )
+ throw new TypeError(`No method named "${t}"`);
+ e[t]();
+ }
+ } else e.to(t);
+ });
+ }
+ }
+ N.on(document, bt, "[data-bs-slide], [data-bs-slide-to]", function (t) {
+ const e = z.getElementFromSelector(this);
+ if (!e || !e.classList.contains(vt)) return;
+ t.preventDefault();
+ const i = xt.getOrCreateInstance(e),
+ n = this.getAttribute("data-bs-slide-to");
+ return n
+ ? (i.to(n), void i._maybeEnableCycle())
+ : "next" === F.getDataAttribute(this, "slide")
+ ? (i.next(), void i._maybeEnableCycle())
+ : (i.prev(), void i._maybeEnableCycle());
+ }),
+ N.on(window, _t, () => {
+ const t = z.find('[data-bs-ride="carousel"]');
+ for (const e of t) xt.getOrCreateInstance(e);
+ }),
+ m(xt);
+ const kt = ".bs.collapse",
+ Lt = `show${kt}`,
+ St = `shown${kt}`,
+ Dt = `hide${kt}`,
+ $t = `hidden${kt}`,
+ It = `click${kt}.data-api`,
+ Nt = "show",
+ Pt = "collapse",
+ Mt = "collapsing",
+ jt = `:scope .${Pt} .${Pt}`,
+ Ft = '[data-bs-toggle="collapse"]',
+ Ht = { parent: null, toggle: !0 },
+ Wt = { parent: "(null|element)", toggle: "boolean" };
+ class Bt extends W {
+ constructor(t, e) {
+ super(t, e),
+ (this._isTransitioning = !1),
+ (this._triggerArray = []);
+ const i = z.find(Ft);
+ for (const t of i) {
+ const e = z.getSelectorFromElement(t),
+ i = z.find(e).filter((t) => t === this._element);
+ null !== e && i.length && this._triggerArray.push(t);
+ }
+ this._initializeChildren(),
+ this._config.parent ||
+ this._addAriaAndCollapsedClass(
+ this._triggerArray,
+ this._isShown()
+ ),
+ this._config.toggle && this.toggle();
+ }
+ static get Default() {
+ return Ht;
+ }
+ static get DefaultType() {
+ return Wt;
+ }
+ static get NAME() {
+ return "collapse";
+ }
+ toggle() {
+ this._isShown() ? this.hide() : this.show();
+ }
+ show() {
+ if (this._isTransitioning || this._isShown()) return;
+ let t = [];
+ if (
+ (this._config.parent &&
+ (t = this._getFirstLevelChildren(
+ ".collapse.show, .collapse.collapsing"
+ )
+ .filter((t) => t !== this._element)
+ .map((t) => Bt.getOrCreateInstance(t, { toggle: !1 }))),
+ t.length && t[0]._isTransitioning)
+ )
+ return;
+ if (N.trigger(this._element, Lt).defaultPrevented) return;
+ for (const e of t) e.hide();
+ const e = this._getDimension();
+ this._element.classList.remove(Pt),
+ this._element.classList.add(Mt),
+ (this._element.style[e] = 0),
+ this._addAriaAndCollapsedClass(this._triggerArray, !0),
+ (this._isTransitioning = !0);
+ const i = `scroll${e[0].toUpperCase() + e.slice(1)}`;
+ this._queueCallback(
+ () => {
+ (this._isTransitioning = !1),
+ this._element.classList.remove(Mt),
+ this._element.classList.add(Pt, Nt),
+ (this._element.style[e] = ""),
+ N.trigger(this._element, St);
+ },
+ this._element,
+ !0
+ ),
+ (this._element.style[e] = `${this._element[i]}px`);
+ }
+ hide() {
+ if (this._isTransitioning || !this._isShown()) return;
+ if (N.trigger(this._element, Dt).defaultPrevented) return;
+ const t = this._getDimension();
+ (this._element.style[t] = `${
+ this._element.getBoundingClientRect()[t]
+ }px`),
+ d(this._element),
+ this._element.classList.add(Mt),
+ this._element.classList.remove(Pt, Nt);
+ for (const t of this._triggerArray) {
+ const e = z.getElementFromSelector(t);
+ e &&
+ !this._isShown(e) &&
+ this._addAriaAndCollapsedClass([t], !1);
+ }
+ (this._isTransitioning = !0),
+ (this._element.style[t] = ""),
+ this._queueCallback(
+ () => {
+ (this._isTransitioning = !1),
+ this._element.classList.remove(Mt),
+ this._element.classList.add(Pt),
+ N.trigger(this._element, $t);
+ },
+ this._element,
+ !0
+ );
+ }
+ _isShown(t = this._element) {
+ return t.classList.contains(Nt);
+ }
+ _configAfterMerge(t) {
+ return (t.toggle = Boolean(t.toggle)), (t.parent = r(t.parent)), t;
+ }
+ _getDimension() {
+ return this._element.classList.contains("collapse-horizontal")
+ ? "width"
+ : "height";
+ }
+ _initializeChildren() {
+ if (!this._config.parent) return;
+ const t = this._getFirstLevelChildren(Ft);
+ for (const e of t) {
+ const t = z.getElementFromSelector(e);
+ t && this._addAriaAndCollapsedClass([e], this._isShown(t));
+ }
+ }
+ _getFirstLevelChildren(t) {
+ const e = z.find(jt, this._config.parent);
+ return z.find(t, this._config.parent).filter((t) => !e.includes(t));
+ }
+ _addAriaAndCollapsedClass(t, e) {
+ if (t.length)
+ for (const i of t)
+ i.classList.toggle("collapsed", !e),
+ i.setAttribute("aria-expanded", e);
+ }
+ static jQueryInterface(t) {
+ const e = {};
+ return (
+ "string" == typeof t && /show|hide/.test(t) && (e.toggle = !1),
+ this.each(function () {
+ const i = Bt.getOrCreateInstance(this, e);
+ if ("string" == typeof t) {
+ if (void 0 === i[t])
+ throw new TypeError(`No method named "${t}"`);
+ i[t]();
+ }
+ })
+ );
+ }
+ }
+ N.on(document, It, Ft, function (t) {
+ ("A" === t.target.tagName ||
+ (t.delegateTarget && "A" === t.delegateTarget.tagName)) &&
+ t.preventDefault();
+ for (const t of z.getMultipleElementsFromSelector(this))
+ Bt.getOrCreateInstance(t, { toggle: !1 }).toggle();
+ }),
+ m(Bt);
+ var zt = "top",
+ Rt = "bottom",
+ qt = "right",
+ Vt = "left",
+ Kt = "auto",
+ Qt = [zt, Rt, qt, Vt],
+ Xt = "start",
+ Yt = "end",
+ Ut = "clippingParents",
+ Gt = "viewport",
+ Jt = "popper",
+ Zt = "reference",
+ te = Qt.reduce(function (t, e) {
+ return t.concat([e + "-" + Xt, e + "-" + Yt]);
+ }, []),
+ ee = [].concat(Qt, [Kt]).reduce(function (t, e) {
+ return t.concat([e, e + "-" + Xt, e + "-" + Yt]);
+ }, []),
+ ie = "beforeRead",
+ ne = "read",
+ se = "afterRead",
+ oe = "beforeMain",
+ re = "main",
+ ae = "afterMain",
+ le = "beforeWrite",
+ ce = "write",
+ he = "afterWrite",
+ de = [ie, ne, se, oe, re, ae, le, ce, he];
+ function ue(t) {
+ return t ? (t.nodeName || "").toLowerCase() : null;
+ }
+ function fe(t) {
+ if (null == t) return window;
+ if ("[object Window]" !== t.toString()) {
+ var e = t.ownerDocument;
+ return (e && e.defaultView) || window;
+ }
+ return t;
+ }
+ function pe(t) {
+ return t instanceof fe(t).Element || t instanceof Element;
+ }
+ function me(t) {
+ return t instanceof fe(t).HTMLElement || t instanceof HTMLElement;
+ }
+ function ge(t) {
+ return (
+ "undefined" != typeof ShadowRoot &&
+ (t instanceof fe(t).ShadowRoot || t instanceof ShadowRoot)
+ );
+ }
+ const _e = {
+ name: "applyStyles",
+ enabled: !0,
+ phase: "write",
+ fn: function (t) {
+ var e = t.state;
+ Object.keys(e.elements).forEach(function (t) {
+ var i = e.styles[t] || {},
+ n = e.attributes[t] || {},
+ s = e.elements[t];
+ me(s) &&
+ ue(s) &&
+ (Object.assign(s.style, i),
+ Object.keys(n).forEach(function (t) {
+ var e = n[t];
+ !1 === e
+ ? s.removeAttribute(t)
+ : s.setAttribute(t, !0 === e ? "" : e);
+ }));
+ });
+ },
+ effect: function (t) {
+ var e = t.state,
+ i = {
+ popper: {
+ position: e.options.strategy,
+ left: "0",
+ top: "0",
+ margin: "0",
+ },
+ arrow: { position: "absolute" },
+ reference: {},
+ };
+ return (
+ Object.assign(e.elements.popper.style, i.popper),
+ (e.styles = i),
+ e.elements.arrow &&
+ Object.assign(e.elements.arrow.style, i.arrow),
+ function () {
+ Object.keys(e.elements).forEach(function (t) {
+ var n = e.elements[t],
+ s = e.attributes[t] || {},
+ o = Object.keys(
+ e.styles.hasOwnProperty(t) ? e.styles[t] : i[t]
+ ).reduce(function (t, e) {
+ return (t[e] = ""), t;
+ }, {});
+ me(n) &&
+ ue(n) &&
+ (Object.assign(n.style, o),
+ Object.keys(s).forEach(function (t) {
+ n.removeAttribute(t);
+ }));
+ });
+ }
+ );
+ },
+ requires: ["computeStyles"],
+ };
+ function be(t) {
+ return t.split("-")[0];
+ }
+ var ve = Math.max,
+ ye = Math.min,
+ we = Math.round;
+ function Ae() {
+ var t = navigator.userAgentData;
+ return null != t && t.brands && Array.isArray(t.brands)
+ ? t.brands
+ .map(function (t) {
+ return t.brand + "/" + t.version;
+ })
+ .join(" ")
+ : navigator.userAgent;
+ }
+ function Ee() {
+ return !/^((?!chrome|android).)*safari/i.test(Ae());
+ }
+ function Te(t, e, i) {
+ void 0 === e && (e = !1), void 0 === i && (i = !1);
+ var n = t.getBoundingClientRect(),
+ s = 1,
+ o = 1;
+ e &&
+ me(t) &&
+ ((s = (t.offsetWidth > 0 && we(n.width) / t.offsetWidth) || 1),
+ (o = (t.offsetHeight > 0 && we(n.height) / t.offsetHeight) || 1));
+ var r = (pe(t) ? fe(t) : window).visualViewport,
+ a = !Ee() && i,
+ l = (n.left + (a && r ? r.offsetLeft : 0)) / s,
+ c = (n.top + (a && r ? r.offsetTop : 0)) / o,
+ h = n.width / s,
+ d = n.height / o;
+ return {
+ width: h,
+ height: d,
+ top: c,
+ right: l + h,
+ bottom: c + d,
+ left: l,
+ x: l,
+ y: c,
+ };
+ }
+ function Ce(t) {
+ var e = Te(t),
+ i = t.offsetWidth,
+ n = t.offsetHeight;
+ return (
+ Math.abs(e.width - i) <= 1 && (i = e.width),
+ Math.abs(e.height - n) <= 1 && (n = e.height),
+ { x: t.offsetLeft, y: t.offsetTop, width: i, height: n }
+ );
+ }
+ function Oe(t, e) {
+ var i = e.getRootNode && e.getRootNode();
+ if (t.contains(e)) return !0;
+ if (i && ge(i)) {
+ var n = e;
+ do {
+ if (n && t.isSameNode(n)) return !0;
+ n = n.parentNode || n.host;
+ } while (n);
+ }
+ return !1;
+ }
+ function xe(t) {
+ return fe(t).getComputedStyle(t);
+ }
+ function ke(t) {
+ return ["table", "td", "th"].indexOf(ue(t)) >= 0;
+ }
+ function Le(t) {
+ return (
+ (pe(t) ? t.ownerDocument : t.document) || window.document
+ ).documentElement;
+ }
+ function Se(t) {
+ return "html" === ue(t)
+ ? t
+ : t.assignedSlot ||
+ t.parentNode ||
+ (ge(t) ? t.host : null) ||
+ Le(t);
+ }
+ function De(t) {
+ return me(t) && "fixed" !== xe(t).position ? t.offsetParent : null;
+ }
+ function $e(t) {
+ for (
+ var e = fe(t), i = De(t);
+ i && ke(i) && "static" === xe(i).position;
+
+ )
+ i = De(i);
+ return i &&
+ ("html" === ue(i) ||
+ ("body" === ue(i) && "static" === xe(i).position))
+ ? e
+ : i ||
+ (function (t) {
+ var e = /firefox/i.test(Ae());
+ if (
+ /Trident/i.test(Ae()) &&
+ me(t) &&
+ "fixed" === xe(t).position
+ )
+ return null;
+ var i = Se(t);
+ for (
+ ge(i) && (i = i.host);
+ me(i) && ["html", "body"].indexOf(ue(i)) < 0;
+
+ ) {
+ var n = xe(i);
+ if (
+ "none" !== n.transform ||
+ "none" !== n.perspective ||
+ "paint" === n.contain ||
+ -1 !==
+ ["transform", "perspective"].indexOf(
+ n.willChange
+ ) ||
+ (e && "filter" === n.willChange) ||
+ (e && n.filter && "none" !== n.filter)
+ )
+ return i;
+ i = i.parentNode;
+ }
+ return null;
+ })(t) ||
+ e;
+ }
+ function Ie(t) {
+ return ["top", "bottom"].indexOf(t) >= 0 ? "x" : "y";
+ }
+ function Ne(t, e, i) {
+ return ve(t, ye(e, i));
+ }
+ function Pe(t) {
+ return Object.assign({}, { top: 0, right: 0, bottom: 0, left: 0 }, t);
+ }
+ function Me(t, e) {
+ return e.reduce(function (e, i) {
+ return (e[i] = t), e;
+ }, {});
+ }
+ const je = {
+ name: "arrow",
+ enabled: !0,
+ phase: "main",
+ fn: function (t) {
+ var e,
+ i = t.state,
+ n = t.name,
+ s = t.options,
+ o = i.elements.arrow,
+ r = i.modifiersData.popperOffsets,
+ a = be(i.placement),
+ l = Ie(a),
+ c = [Vt, qt].indexOf(a) >= 0 ? "height" : "width";
+ if (o && r) {
+ var h = (function (t, e) {
+ return Pe(
+ "number" !=
+ typeof (t =
+ "function" == typeof t
+ ? t(
+ Object.assign({}, e.rects, {
+ placement: e.placement,
+ })
+ )
+ : t)
+ ? t
+ : Me(t, Qt)
+ );
+ })(s.padding, i),
+ d = Ce(o),
+ u = "y" === l ? zt : Vt,
+ f = "y" === l ? Rt : qt,
+ p =
+ i.rects.reference[c] +
+ i.rects.reference[l] -
+ r[l] -
+ i.rects.popper[c],
+ m = r[l] - i.rects.reference[l],
+ g = $e(o),
+ _ = g
+ ? "y" === l
+ ? g.clientHeight || 0
+ : g.clientWidth || 0
+ : 0,
+ b = p / 2 - m / 2,
+ v = h[u],
+ y = _ - d[c] - h[f],
+ w = _ / 2 - d[c] / 2 + b,
+ A = Ne(v, w, y),
+ E = l;
+ i.modifiersData[n] =
+ (((e = {})[E] = A), (e.centerOffset = A - w), e);
+ }
+ },
+ effect: function (t) {
+ var e = t.state,
+ i = t.options.element,
+ n = void 0 === i ? "[data-popper-arrow]" : i;
+ null != n &&
+ ("string" != typeof n ||
+ (n = e.elements.popper.querySelector(n))) &&
+ Oe(e.elements.popper, n) &&
+ (e.elements.arrow = n);
+ },
+ requires: ["popperOffsets"],
+ requiresIfExists: ["preventOverflow"],
+ };
+ function Fe(t) {
+ return t.split("-")[1];
+ }
+ var He = { top: "auto", right: "auto", bottom: "auto", left: "auto" };
+ function We(t) {
+ var e,
+ i = t.popper,
+ n = t.popperRect,
+ s = t.placement,
+ o = t.variation,
+ r = t.offsets,
+ a = t.position,
+ l = t.gpuAcceleration,
+ c = t.adaptive,
+ h = t.roundOffsets,
+ d = t.isFixed,
+ u = r.x,
+ f = void 0 === u ? 0 : u,
+ p = r.y,
+ m = void 0 === p ? 0 : p,
+ g = "function" == typeof h ? h({ x: f, y: m }) : { x: f, y: m };
+ (f = g.x), (m = g.y);
+ var _ = r.hasOwnProperty("x"),
+ b = r.hasOwnProperty("y"),
+ v = Vt,
+ y = zt,
+ w = window;
+ if (c) {
+ var A = $e(i),
+ E = "clientHeight",
+ T = "clientWidth";
+ A === fe(i) &&
+ "static" !== xe((A = Le(i))).position &&
+ "absolute" === a &&
+ ((E = "scrollHeight"), (T = "scrollWidth")),
+ (s === zt || ((s === Vt || s === qt) && o === Yt)) &&
+ ((y = Rt),
+ (m -=
+ (d && A === w && w.visualViewport
+ ? w.visualViewport.height
+ : A[E]) - n.height),
+ (m *= l ? 1 : -1)),
+ (s !== Vt && ((s !== zt && s !== Rt) || o !== Yt)) ||
+ ((v = qt),
+ (f -=
+ (d && A === w && w.visualViewport
+ ? w.visualViewport.width
+ : A[T]) - n.width),
+ (f *= l ? 1 : -1));
+ }
+ var C,
+ O = Object.assign({ position: a }, c && He),
+ x =
+ !0 === h
+ ? (function (t, e) {
+ var i = t.x,
+ n = t.y,
+ s = e.devicePixelRatio || 1;
+ return {
+ x: we(i * s) / s || 0,
+ y: we(n * s) / s || 0,
+ };
+ })({ x: f, y: m }, fe(i))
+ : { x: f, y: m };
+ return (
+ (f = x.x),
+ (m = x.y),
+ l
+ ? Object.assign(
+ {},
+ O,
+ (((C = {})[y] = b ? "0" : ""),
+ (C[v] = _ ? "0" : ""),
+ (C.transform =
+ (w.devicePixelRatio || 1) <= 1
+ ? "translate(" + f + "px, " + m + "px)"
+ : "translate3d(" + f + "px, " + m + "px, 0)"),
+ C)
+ )
+ : Object.assign(
+ {},
+ O,
+ (((e = {})[y] = b ? m + "px" : ""),
+ (e[v] = _ ? f + "px" : ""),
+ (e.transform = ""),
+ e)
+ )
+ );
+ }
+ const Be = {
+ name: "computeStyles",
+ enabled: !0,
+ phase: "beforeWrite",
+ fn: function (t) {
+ var e = t.state,
+ i = t.options,
+ n = i.gpuAcceleration,
+ s = void 0 === n || n,
+ o = i.adaptive,
+ r = void 0 === o || o,
+ a = i.roundOffsets,
+ l = void 0 === a || a,
+ c = {
+ placement: be(e.placement),
+ variation: Fe(e.placement),
+ popper: e.elements.popper,
+ popperRect: e.rects.popper,
+ gpuAcceleration: s,
+ isFixed: "fixed" === e.options.strategy,
+ };
+ null != e.modifiersData.popperOffsets &&
+ (e.styles.popper = Object.assign(
+ {},
+ e.styles.popper,
+ We(
+ Object.assign({}, c, {
+ offsets: e.modifiersData.popperOffsets,
+ position: e.options.strategy,
+ adaptive: r,
+ roundOffsets: l,
+ })
+ )
+ )),
+ null != e.modifiersData.arrow &&
+ (e.styles.arrow = Object.assign(
+ {},
+ e.styles.arrow,
+ We(
+ Object.assign({}, c, {
+ offsets: e.modifiersData.arrow,
+ position: "absolute",
+ adaptive: !1,
+ roundOffsets: l,
+ })
+ )
+ )),
+ (e.attributes.popper = Object.assign({}, e.attributes.popper, {
+ "data-popper-placement": e.placement,
+ }));
+ },
+ data: {},
+ };
+ var ze = { passive: !0 };
+ const Re = {
+ name: "eventListeners",
+ enabled: !0,
+ phase: "write",
+ fn: function () {},
+ effect: function (t) {
+ var e = t.state,
+ i = t.instance,
+ n = t.options,
+ s = n.scroll,
+ o = void 0 === s || s,
+ r = n.resize,
+ a = void 0 === r || r,
+ l = fe(e.elements.popper),
+ c = [].concat(
+ e.scrollParents.reference,
+ e.scrollParents.popper
+ );
+ return (
+ o &&
+ c.forEach(function (t) {
+ t.addEventListener("scroll", i.update, ze);
+ }),
+ a && l.addEventListener("resize", i.update, ze),
+ function () {
+ o &&
+ c.forEach(function (t) {
+ t.removeEventListener("scroll", i.update, ze);
+ }),
+ a && l.removeEventListener("resize", i.update, ze);
+ }
+ );
+ },
+ data: {},
+ };
+ var qe = { left: "right", right: "left", bottom: "top", top: "bottom" };
+ function Ve(t) {
+ return t.replace(/left|right|bottom|top/g, function (t) {
+ return qe[t];
+ });
+ }
+ var Ke = { start: "end", end: "start" };
+ function Qe(t) {
+ return t.replace(/start|end/g, function (t) {
+ return Ke[t];
+ });
+ }
+ function Xe(t) {
+ var e = fe(t);
+ return { scrollLeft: e.pageXOffset, scrollTop: e.pageYOffset };
+ }
+ function Ye(t) {
+ return Te(Le(t)).left + Xe(t).scrollLeft;
+ }
+ function Ue(t) {
+ var e = xe(t),
+ i = e.overflow,
+ n = e.overflowX,
+ s = e.overflowY;
+ return /auto|scroll|overlay|hidden/.test(i + s + n);
+ }
+ function Ge(t) {
+ return ["html", "body", "#document"].indexOf(ue(t)) >= 0
+ ? t.ownerDocument.body
+ : me(t) && Ue(t)
+ ? t
+ : Ge(Se(t));
+ }
+ function Je(t, e) {
+ var i;
+ void 0 === e && (e = []);
+ var n = Ge(t),
+ s = n === (null == (i = t.ownerDocument) ? void 0 : i.body),
+ o = fe(n),
+ r = s ? [o].concat(o.visualViewport || [], Ue(n) ? n : []) : n,
+ a = e.concat(r);
+ return s ? a : a.concat(Je(Se(r)));
+ }
+ function Ze(t) {
+ return Object.assign({}, t, {
+ left: t.x,
+ top: t.y,
+ right: t.x + t.width,
+ bottom: t.y + t.height,
+ });
+ }
+ function ti(t, e, i) {
+ return e === Gt
+ ? Ze(
+ (function (t, e) {
+ var i = fe(t),
+ n = Le(t),
+ s = i.visualViewport,
+ o = n.clientWidth,
+ r = n.clientHeight,
+ a = 0,
+ l = 0;
+ if (s) {
+ (o = s.width), (r = s.height);
+ var c = Ee();
+ (c || (!c && "fixed" === e)) &&
+ ((a = s.offsetLeft), (l = s.offsetTop));
+ }
+ return { width: o, height: r, x: a + Ye(t), y: l };
+ })(t, i)
+ )
+ : pe(e)
+ ? (function (t, e) {
+ var i = Te(t, !1, "fixed" === e);
+ return (
+ (i.top = i.top + t.clientTop),
+ (i.left = i.left + t.clientLeft),
+ (i.bottom = i.top + t.clientHeight),
+ (i.right = i.left + t.clientWidth),
+ (i.width = t.clientWidth),
+ (i.height = t.clientHeight),
+ (i.x = i.left),
+ (i.y = i.top),
+ i
+ );
+ })(e, i)
+ : Ze(
+ (function (t) {
+ var e,
+ i = Le(t),
+ n = Xe(t),
+ s = null == (e = t.ownerDocument) ? void 0 : e.body,
+ o = ve(
+ i.scrollWidth,
+ i.clientWidth,
+ s ? s.scrollWidth : 0,
+ s ? s.clientWidth : 0
+ ),
+ r = ve(
+ i.scrollHeight,
+ i.clientHeight,
+ s ? s.scrollHeight : 0,
+ s ? s.clientHeight : 0
+ ),
+ a = -n.scrollLeft + Ye(t),
+ l = -n.scrollTop;
+ return (
+ "rtl" === xe(s || i).direction &&
+ (a +=
+ ve(i.clientWidth, s ? s.clientWidth : 0) - o),
+ { width: o, height: r, x: a, y: l }
+ );
+ })(Le(t))
+ );
+ }
+ function ei(t) {
+ var e,
+ i = t.reference,
+ n = t.element,
+ s = t.placement,
+ o = s ? be(s) : null,
+ r = s ? Fe(s) : null,
+ a = i.x + i.width / 2 - n.width / 2,
+ l = i.y + i.height / 2 - n.height / 2;
+ switch (o) {
+ case zt:
+ e = { x: a, y: i.y - n.height };
+ break;
+ case Rt:
+ e = { x: a, y: i.y + i.height };
+ break;
+ case qt:
+ e = { x: i.x + i.width, y: l };
+ break;
+ case Vt:
+ e = { x: i.x - n.width, y: l };
+ break;
+ default:
+ e = { x: i.x, y: i.y };
+ }
+ var c = o ? Ie(o) : null;
+ if (null != c) {
+ var h = "y" === c ? "height" : "width";
+ switch (r) {
+ case Xt:
+ e[c] = e[c] - (i[h] / 2 - n[h] / 2);
+ break;
+ case Yt:
+ e[c] = e[c] + (i[h] / 2 - n[h] / 2);
+ }
+ }
+ return e;
+ }
+ function ii(t, e) {
+ void 0 === e && (e = {});
+ var i = e,
+ n = i.placement,
+ s = void 0 === n ? t.placement : n,
+ o = i.strategy,
+ r = void 0 === o ? t.strategy : o,
+ a = i.boundary,
+ l = void 0 === a ? Ut : a,
+ c = i.rootBoundary,
+ h = void 0 === c ? Gt : c,
+ d = i.elementContext,
+ u = void 0 === d ? Jt : d,
+ f = i.altBoundary,
+ p = void 0 !== f && f,
+ m = i.padding,
+ g = void 0 === m ? 0 : m,
+ _ = Pe("number" != typeof g ? g : Me(g, Qt)),
+ b = u === Jt ? Zt : Jt,
+ v = t.rects.popper,
+ y = t.elements[p ? b : u],
+ w = (function (t, e, i, n) {
+ var s =
+ "clippingParents" === e
+ ? (function (t) {
+ var e = Je(Se(t)),
+ i =
+ ["absolute", "fixed"].indexOf(
+ xe(t).position
+ ) >= 0 && me(t)
+ ? $e(t)
+ : t;
+ return pe(i)
+ ? e.filter(function (t) {
+ return (
+ pe(t) &&
+ Oe(t, i) &&
+ "body" !== ue(t)
+ );
+ })
+ : [];
+ })(t)
+ : [].concat(e),
+ o = [].concat(s, [i]),
+ r = o[0],
+ a = o.reduce(function (e, i) {
+ var s = ti(t, i, n);
+ return (
+ (e.top = ve(s.top, e.top)),
+ (e.right = ye(s.right, e.right)),
+ (e.bottom = ye(s.bottom, e.bottom)),
+ (e.left = ve(s.left, e.left)),
+ e
+ );
+ }, ti(t, r, n));
+ return (
+ (a.width = a.right - a.left),
+ (a.height = a.bottom - a.top),
+ (a.x = a.left),
+ (a.y = a.top),
+ a
+ );
+ })(pe(y) ? y : y.contextElement || Le(t.elements.popper), l, h, r),
+ A = Te(t.elements.reference),
+ E = ei({
+ reference: A,
+ element: v,
+ strategy: "absolute",
+ placement: s,
+ }),
+ T = Ze(Object.assign({}, v, E)),
+ C = u === Jt ? T : A,
+ O = {
+ top: w.top - C.top + _.top,
+ bottom: C.bottom - w.bottom + _.bottom,
+ left: w.left - C.left + _.left,
+ right: C.right - w.right + _.right,
+ },
+ x = t.modifiersData.offset;
+ if (u === Jt && x) {
+ var k = x[s];
+ Object.keys(O).forEach(function (t) {
+ var e = [qt, Rt].indexOf(t) >= 0 ? 1 : -1,
+ i = [zt, Rt].indexOf(t) >= 0 ? "y" : "x";
+ O[t] += k[i] * e;
+ });
+ }
+ return O;
+ }
+ function ni(t, e) {
+ void 0 === e && (e = {});
+ var i = e,
+ n = i.placement,
+ s = i.boundary,
+ o = i.rootBoundary,
+ r = i.padding,
+ a = i.flipVariations,
+ l = i.allowedAutoPlacements,
+ c = void 0 === l ? ee : l,
+ h = Fe(n),
+ d = h
+ ? a
+ ? te
+ : te.filter(function (t) {
+ return Fe(t) === h;
+ })
+ : Qt,
+ u = d.filter(function (t) {
+ return c.indexOf(t) >= 0;
+ });
+ 0 === u.length && (u = d);
+ var f = u.reduce(function (e, i) {
+ return (
+ (e[i] = ii(t, {
+ placement: i,
+ boundary: s,
+ rootBoundary: o,
+ padding: r,
+ })[be(i)]),
+ e
+ );
+ }, {});
+ return Object.keys(f).sort(function (t, e) {
+ return f[t] - f[e];
+ });
+ }
+ const si = {
+ name: "flip",
+ enabled: !0,
+ phase: "main",
+ fn: function (t) {
+ var e = t.state,
+ i = t.options,
+ n = t.name;
+ if (!e.modifiersData[n]._skip) {
+ for (
+ var s = i.mainAxis,
+ o = void 0 === s || s,
+ r = i.altAxis,
+ a = void 0 === r || r,
+ l = i.fallbackPlacements,
+ c = i.padding,
+ h = i.boundary,
+ d = i.rootBoundary,
+ u = i.altBoundary,
+ f = i.flipVariations,
+ p = void 0 === f || f,
+ m = i.allowedAutoPlacements,
+ g = e.options.placement,
+ _ = be(g),
+ b =
+ l ||
+ (_ !== g && p
+ ? (function (t) {
+ if (be(t) === Kt) return [];
+ var e = Ve(t);
+ return [Qe(t), e, Qe(e)];
+ })(g)
+ : [Ve(g)]),
+ v = [g].concat(b).reduce(function (t, i) {
+ return t.concat(
+ be(i) === Kt
+ ? ni(e, {
+ placement: i,
+ boundary: h,
+ rootBoundary: d,
+ padding: c,
+ flipVariations: p,
+ allowedAutoPlacements: m,
+ })
+ : i
+ );
+ }, []),
+ y = e.rects.reference,
+ w = e.rects.popper,
+ A = new Map(),
+ E = !0,
+ T = v[0],
+ C = 0;
+ C < v.length;
+ C++
+ ) {
+ var O = v[C],
+ x = be(O),
+ k = Fe(O) === Xt,
+ L = [zt, Rt].indexOf(x) >= 0,
+ S = L ? "width" : "height",
+ D = ii(e, {
+ placement: O,
+ boundary: h,
+ rootBoundary: d,
+ altBoundary: u,
+ padding: c,
+ }),
+ $ = L ? (k ? qt : Vt) : k ? Rt : zt;
+ y[S] > w[S] && ($ = Ve($));
+ var I = Ve($),
+ N = [];
+ if (
+ (o && N.push(D[x] <= 0),
+ a && N.push(D[$] <= 0, D[I] <= 0),
+ N.every(function (t) {
+ return t;
+ }))
+ ) {
+ (T = O), (E = !1);
+ break;
+ }
+ A.set(O, N);
+ }
+ if (E)
+ for (
+ var P = function (t) {
+ var e = v.find(function (e) {
+ var i = A.get(e);
+ if (i)
+ return i
+ .slice(0, t)
+ .every(function (t) {
+ return t;
+ });
+ });
+ if (e) return (T = e), "break";
+ },
+ M = p ? 3 : 1;
+ M > 0 && "break" !== P(M);
+ M--
+ );
+ e.placement !== T &&
+ ((e.modifiersData[n]._skip = !0),
+ (e.placement = T),
+ (e.reset = !0));
+ }
+ },
+ requiresIfExists: ["offset"],
+ data: { _skip: !1 },
+ };
+ function oi(t, e, i) {
+ return (
+ void 0 === i && (i = { x: 0, y: 0 }),
+ {
+ top: t.top - e.height - i.y,
+ right: t.right - e.width + i.x,
+ bottom: t.bottom - e.height + i.y,
+ left: t.left - e.width - i.x,
+ }
+ );
+ }
+ function ri(t) {
+ return [zt, qt, Rt, Vt].some(function (e) {
+ return t[e] >= 0;
+ });
+ }
+ const ai = {
+ name: "hide",
+ enabled: !0,
+ phase: "main",
+ requiresIfExists: ["preventOverflow"],
+ fn: function (t) {
+ var e = t.state,
+ i = t.name,
+ n = e.rects.reference,
+ s = e.rects.popper,
+ o = e.modifiersData.preventOverflow,
+ r = ii(e, { elementContext: "reference" }),
+ a = ii(e, { altBoundary: !0 }),
+ l = oi(r, n),
+ c = oi(a, s, o),
+ h = ri(l),
+ d = ri(c);
+ (e.modifiersData[i] = {
+ referenceClippingOffsets: l,
+ popperEscapeOffsets: c,
+ isReferenceHidden: h,
+ hasPopperEscaped: d,
+ }),
+ (e.attributes.popper = Object.assign(
+ {},
+ e.attributes.popper,
+ {
+ "data-popper-reference-hidden": h,
+ "data-popper-escaped": d,
+ }
+ ));
+ },
+ },
+ li = {
+ name: "offset",
+ enabled: !0,
+ phase: "main",
+ requires: ["popperOffsets"],
+ fn: function (t) {
+ var e = t.state,
+ i = t.options,
+ n = t.name,
+ s = i.offset,
+ o = void 0 === s ? [0, 0] : s,
+ r = ee.reduce(function (t, i) {
+ return (
+ (t[i] = (function (t, e, i) {
+ var n = be(t),
+ s = [Vt, zt].indexOf(n) >= 0 ? -1 : 1,
+ o =
+ "function" == typeof i
+ ? i(
+ Object.assign({}, e, {
+ placement: t,
+ })
+ )
+ : i,
+ r = o[0],
+ a = o[1];
+ return (
+ (r = r || 0),
+ (a = (a || 0) * s),
+ [Vt, qt].indexOf(n) >= 0
+ ? { x: a, y: r }
+ : { x: r, y: a }
+ );
+ })(i, e.rects, o)),
+ t
+ );
+ }, {}),
+ a = r[e.placement],
+ l = a.x,
+ c = a.y;
+ null != e.modifiersData.popperOffsets &&
+ ((e.modifiersData.popperOffsets.x += l),
+ (e.modifiersData.popperOffsets.y += c)),
+ (e.modifiersData[n] = r);
+ },
+ },
+ ci = {
+ name: "popperOffsets",
+ enabled: !0,
+ phase: "read",
+ fn: function (t) {
+ var e = t.state,
+ i = t.name;
+ e.modifiersData[i] = ei({
+ reference: e.rects.reference,
+ element: e.rects.popper,
+ strategy: "absolute",
+ placement: e.placement,
+ });
+ },
+ data: {},
+ },
+ hi = {
+ name: "preventOverflow",
+ enabled: !0,
+ phase: "main",
+ fn: function (t) {
+ var e = t.state,
+ i = t.options,
+ n = t.name,
+ s = i.mainAxis,
+ o = void 0 === s || s,
+ r = i.altAxis,
+ a = void 0 !== r && r,
+ l = i.boundary,
+ c = i.rootBoundary,
+ h = i.altBoundary,
+ d = i.padding,
+ u = i.tether,
+ f = void 0 === u || u,
+ p = i.tetherOffset,
+ m = void 0 === p ? 0 : p,
+ g = ii(e, {
+ boundary: l,
+ rootBoundary: c,
+ padding: d,
+ altBoundary: h,
+ }),
+ _ = be(e.placement),
+ b = Fe(e.placement),
+ v = !b,
+ y = Ie(_),
+ w = "x" === y ? "y" : "x",
+ A = e.modifiersData.popperOffsets,
+ E = e.rects.reference,
+ T = e.rects.popper,
+ C =
+ "function" == typeof m
+ ? m(
+ Object.assign({}, e.rects, {
+ placement: e.placement,
+ })
+ )
+ : m,
+ O =
+ "number" == typeof C
+ ? { mainAxis: C, altAxis: C }
+ : Object.assign({ mainAxis: 0, altAxis: 0 }, C),
+ x = e.modifiersData.offset
+ ? e.modifiersData.offset[e.placement]
+ : null,
+ k = { x: 0, y: 0 };
+ if (A) {
+ if (o) {
+ var L,
+ S = "y" === y ? zt : Vt,
+ D = "y" === y ? Rt : qt,
+ $ = "y" === y ? "height" : "width",
+ I = A[y],
+ N = I + g[S],
+ P = I - g[D],
+ M = f ? -T[$] / 2 : 0,
+ j = b === Xt ? E[$] : T[$],
+ F = b === Xt ? -T[$] : -E[$],
+ H = e.elements.arrow,
+ W = f && H ? Ce(H) : { width: 0, height: 0 },
+ B = e.modifiersData["arrow#persistent"]
+ ? e.modifiersData["arrow#persistent"].padding
+ : { top: 0, right: 0, bottom: 0, left: 0 },
+ z = B[S],
+ R = B[D],
+ q = Ne(0, E[$], W[$]),
+ V = v
+ ? E[$] / 2 - M - q - z - O.mainAxis
+ : j - q - z - O.mainAxis,
+ K = v
+ ? -E[$] / 2 + M + q + R + O.mainAxis
+ : F + q + R + O.mainAxis,
+ Q = e.elements.arrow && $e(e.elements.arrow),
+ X = Q
+ ? "y" === y
+ ? Q.clientTop || 0
+ : Q.clientLeft || 0
+ : 0,
+ Y = null != (L = null == x ? void 0 : x[y]) ? L : 0,
+ U = I + K - Y,
+ G = Ne(
+ f ? ye(N, I + V - Y - X) : N,
+ I,
+ f ? ve(P, U) : P
+ );
+ (A[y] = G), (k[y] = G - I);
+ }
+ if (a) {
+ var J,
+ Z = "x" === y ? zt : Vt,
+ tt = "x" === y ? Rt : qt,
+ et = A[w],
+ it = "y" === w ? "height" : "width",
+ nt = et + g[Z],
+ st = et - g[tt],
+ ot = -1 !== [zt, Vt].indexOf(_),
+ rt =
+ null != (J = null == x ? void 0 : x[w]) ? J : 0,
+ at = ot ? nt : et - E[it] - T[it] - rt + O.altAxis,
+ lt = ot ? et + E[it] + T[it] - rt - O.altAxis : st,
+ ct =
+ f && ot
+ ? (function (t, e, i) {
+ var n = Ne(t, e, i);
+ return n > i ? i : n;
+ })(at, et, lt)
+ : Ne(f ? at : nt, et, f ? lt : st);
+ (A[w] = ct), (k[w] = ct - et);
+ }
+ e.modifiersData[n] = k;
+ }
+ },
+ requiresIfExists: ["offset"],
+ };
+ function di(t, e, i) {
+ void 0 === i && (i = !1);
+ var n,
+ s,
+ o = me(e),
+ r =
+ me(e) &&
+ (function (t) {
+ var e = t.getBoundingClientRect(),
+ i = we(e.width) / t.offsetWidth || 1,
+ n = we(e.height) / t.offsetHeight || 1;
+ return 1 !== i || 1 !== n;
+ })(e),
+ a = Le(e),
+ l = Te(t, r, i),
+ c = { scrollLeft: 0, scrollTop: 0 },
+ h = { x: 0, y: 0 };
+ return (
+ (o || (!o && !i)) &&
+ (("body" !== ue(e) || Ue(a)) &&
+ (c =
+ (n = e) !== fe(n) && me(n)
+ ? {
+ scrollLeft: (s = n).scrollLeft,
+ scrollTop: s.scrollTop,
+ }
+ : Xe(n)),
+ me(e)
+ ? (((h = Te(e, !0)).x += e.clientLeft),
+ (h.y += e.clientTop))
+ : a && (h.x = Ye(a))),
+ {
+ x: l.left + c.scrollLeft - h.x,
+ y: l.top + c.scrollTop - h.y,
+ width: l.width,
+ height: l.height,
+ }
+ );
+ }
+ function ui(t) {
+ var e = new Map(),
+ i = new Set(),
+ n = [];
+ function s(t) {
+ i.add(t.name),
+ []
+ .concat(t.requires || [], t.requiresIfExists || [])
+ .forEach(function (t) {
+ if (!i.has(t)) {
+ var n = e.get(t);
+ n && s(n);
+ }
+ }),
+ n.push(t);
+ }
+ return (
+ t.forEach(function (t) {
+ e.set(t.name, t);
+ }),
+ t.forEach(function (t) {
+ i.has(t.name) || s(t);
+ }),
+ n
+ );
+ }
+ var fi = { placement: "bottom", modifiers: [], strategy: "absolute" };
+ function pi() {
+ for (var t = arguments.length, e = new Array(t), i = 0; i < t; i++)
+ e[i] = arguments[i];
+ return !e.some(function (t) {
+ return !(t && "function" == typeof t.getBoundingClientRect);
+ });
+ }
+ function mi(t) {
+ void 0 === t && (t = {});
+ var e = t,
+ i = e.defaultModifiers,
+ n = void 0 === i ? [] : i,
+ s = e.defaultOptions,
+ o = void 0 === s ? fi : s;
+ return function (t, e, i) {
+ void 0 === i && (i = o);
+ var s,
+ r,
+ a = {
+ placement: "bottom",
+ orderedModifiers: [],
+ options: Object.assign({}, fi, o),
+ modifiersData: {},
+ elements: { reference: t, popper: e },
+ attributes: {},
+ styles: {},
+ },
+ l = [],
+ c = !1,
+ h = {
+ state: a,
+ setOptions: function (i) {
+ var s = "function" == typeof i ? i(a.options) : i;
+ d(),
+ (a.options = Object.assign({}, o, a.options, s)),
+ (a.scrollParents = {
+ reference: pe(t)
+ ? Je(t)
+ : t.contextElement
+ ? Je(t.contextElement)
+ : [],
+ popper: Je(e),
+ });
+ var r,
+ c,
+ u = (function (t) {
+ var e = ui(t);
+ return de.reduce(function (t, i) {
+ return t.concat(
+ e.filter(function (t) {
+ return t.phase === i;
+ })
+ );
+ }, []);
+ })(
+ ((r = [].concat(n, a.options.modifiers)),
+ (c = r.reduce(function (t, e) {
+ var i = t[e.name];
+ return (
+ (t[e.name] = i
+ ? Object.assign({}, i, e, {
+ options: Object.assign(
+ {},
+ i.options,
+ e.options
+ ),
+ data: Object.assign(
+ {},
+ i.data,
+ e.data
+ ),
+ })
+ : e),
+ t
+ );
+ }, {})),
+ Object.keys(c).map(function (t) {
+ return c[t];
+ }))
+ );
+ return (
+ (a.orderedModifiers = u.filter(function (t) {
+ return t.enabled;
+ })),
+ a.orderedModifiers.forEach(function (t) {
+ var e = t.name,
+ i = t.options,
+ n = void 0 === i ? {} : i,
+ s = t.effect;
+ if ("function" == typeof s) {
+ var o = s({
+ state: a,
+ name: e,
+ instance: h,
+ options: n,
+ });
+ l.push(o || function () {});
+ }
+ }),
+ h.update()
+ );
+ },
+ forceUpdate: function () {
+ if (!c) {
+ var t = a.elements,
+ e = t.reference,
+ i = t.popper;
+ if (pi(e, i)) {
+ (a.rects = {
+ reference: di(
+ e,
+ $e(i),
+ "fixed" === a.options.strategy
+ ),
+ popper: Ce(i),
+ }),
+ (a.reset = !1),
+ (a.placement = a.options.placement),
+ a.orderedModifiers.forEach(function (t) {
+ return (a.modifiersData[t.name] =
+ Object.assign({}, t.data));
+ });
+ for (
+ var n = 0;
+ n < a.orderedModifiers.length;
+ n++
+ )
+ if (!0 !== a.reset) {
+ var s = a.orderedModifiers[n],
+ o = s.fn,
+ r = s.options,
+ l = void 0 === r ? {} : r,
+ d = s.name;
+ "function" == typeof o &&
+ (a =
+ o({
+ state: a,
+ options: l,
+ name: d,
+ instance: h,
+ }) || a);
+ } else (a.reset = !1), (n = -1);
+ }
+ }
+ },
+ update:
+ ((s = function () {
+ return new Promise(function (t) {
+ h.forceUpdate(), t(a);
+ });
+ }),
+ function () {
+ return (
+ r ||
+ (r = new Promise(function (t) {
+ Promise.resolve().then(function () {
+ (r = void 0), t(s());
+ });
+ })),
+ r
+ );
+ }),
+ destroy: function () {
+ d(), (c = !0);
+ },
+ };
+ if (!pi(t, e)) return h;
+ function d() {
+ l.forEach(function (t) {
+ return t();
+ }),
+ (l = []);
+ }
+ return (
+ h.setOptions(i).then(function (t) {
+ !c && i.onFirstUpdate && i.onFirstUpdate(t);
+ }),
+ h
+ );
+ };
+ }
+ var gi = mi(),
+ _i = mi({ defaultModifiers: [Re, ci, Be, _e] }),
+ bi = mi({ defaultModifiers: [Re, ci, Be, _e, li, si, hi, je, ai] });
+ const vi = Object.freeze(
+ Object.defineProperty(
+ {
+ __proto__: null,
+ afterMain: ae,
+ afterRead: se,
+ afterWrite: he,
+ applyStyles: _e,
+ arrow: je,
+ auto: Kt,
+ basePlacements: Qt,
+ beforeMain: oe,
+ beforeRead: ie,
+ beforeWrite: le,
+ bottom: Rt,
+ clippingParents: Ut,
+ computeStyles: Be,
+ createPopper: bi,
+ createPopperBase: gi,
+ createPopperLite: _i,
+ detectOverflow: ii,
+ end: Yt,
+ eventListeners: Re,
+ flip: si,
+ hide: ai,
+ left: Vt,
+ main: re,
+ modifierPhases: de,
+ offset: li,
+ placements: ee,
+ popper: Jt,
+ popperGenerator: mi,
+ popperOffsets: ci,
+ preventOverflow: hi,
+ read: ne,
+ reference: Zt,
+ right: qt,
+ start: Xt,
+ top: zt,
+ variationPlacements: te,
+ viewport: Gt,
+ write: ce,
+ },
+ Symbol.toStringTag,
+ { value: "Module" }
+ )
+ ),
+ yi = "dropdown",
+ wi = ".bs.dropdown",
+ Ai = ".data-api",
+ Ei = "ArrowUp",
+ Ti = "ArrowDown",
+ Ci = `hide${wi}`,
+ Oi = `hidden${wi}`,
+ xi = `show${wi}`,
+ ki = `shown${wi}`,
+ Li = `click${wi}${Ai}`,
+ Si = `keydown${wi}${Ai}`,
+ Di = `keyup${wi}${Ai}`,
+ $i = "show",
+ Ii = '[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',
+ Ni = `${Ii}.${$i}`,
+ Pi = ".dropdown-menu",
+ Mi = p() ? "top-end" : "top-start",
+ ji = p() ? "top-start" : "top-end",
+ Fi = p() ? "bottom-end" : "bottom-start",
+ Hi = p() ? "bottom-start" : "bottom-end",
+ Wi = p() ? "left-start" : "right-start",
+ Bi = p() ? "right-start" : "left-start",
+ zi = {
+ autoClose: !0,
+ boundary: "clippingParents",
+ display: "dynamic",
+ offset: [0, 2],
+ popperConfig: null,
+ reference: "toggle",
+ },
+ Ri = {
+ autoClose: "(boolean|string)",
+ boundary: "(string|element)",
+ display: "string",
+ offset: "(array|string|function)",
+ popperConfig: "(null|object|function)",
+ reference: "(string|element|object)",
+ };
+ class qi extends W {
+ constructor(t, e) {
+ super(t, e),
+ (this._popper = null),
+ (this._parent = this._element.parentNode),
+ (this._menu =
+ z.next(this._element, Pi)[0] ||
+ z.prev(this._element, Pi)[0] ||
+ z.findOne(Pi, this._parent)),
+ (this._inNavbar = this._detectNavbar());
+ }
+ static get Default() {
+ return zi;
+ }
+ static get DefaultType() {
+ return Ri;
+ }
+ static get NAME() {
+ return yi;
+ }
+ toggle() {
+ return this._isShown() ? this.hide() : this.show();
+ }
+ show() {
+ if (l(this._element) || this._isShown()) return;
+ const t = { relatedTarget: this._element };
+ if (!N.trigger(this._element, xi, t).defaultPrevented) {
+ if (
+ (this._createPopper(),
+ "ontouchstart" in document.documentElement &&
+ !this._parent.closest(".navbar-nav"))
+ )
+ for (const t of [].concat(...document.body.children))
+ N.on(t, "mouseover", h);
+ this._element.focus(),
+ this._element.setAttribute("aria-expanded", !0),
+ this._menu.classList.add($i),
+ this._element.classList.add($i),
+ N.trigger(this._element, ki, t);
+ }
+ }
+ hide() {
+ if (l(this._element) || !this._isShown()) return;
+ const t = { relatedTarget: this._element };
+ this._completeHide(t);
+ }
+ dispose() {
+ this._popper && this._popper.destroy(), super.dispose();
+ }
+ update() {
+ (this._inNavbar = this._detectNavbar()),
+ this._popper && this._popper.update();
+ }
+ _completeHide(t) {
+ if (!N.trigger(this._element, Ci, t).defaultPrevented) {
+ if ("ontouchstart" in document.documentElement)
+ for (const t of [].concat(...document.body.children))
+ N.off(t, "mouseover", h);
+ this._popper && this._popper.destroy(),
+ this._menu.classList.remove($i),
+ this._element.classList.remove($i),
+ this._element.setAttribute("aria-expanded", "false"),
+ F.removeDataAttribute(this._menu, "popper"),
+ N.trigger(this._element, Oi, t);
+ }
+ }
+ _getConfig(t) {
+ if (
+ "object" == typeof (t = super._getConfig(t)).reference &&
+ !o(t.reference) &&
+ "function" != typeof t.reference.getBoundingClientRect
+ )
+ throw new TypeError(
+ `${yi.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`
+ );
+ return t;
+ }
+ _createPopper() {
+ if (void 0 === vi)
+ throw new TypeError(
+ "Bootstrap's dropdowns require Popper (https://popper.js.org)"
+ );
+ let t = this._element;
+ "parent" === this._config.reference
+ ? (t = this._parent)
+ : o(this._config.reference)
+ ? (t = r(this._config.reference))
+ : "object" == typeof this._config.reference &&
+ (t = this._config.reference);
+ const e = this._getPopperConfig();
+ this._popper = bi(t, this._menu, e);
+ }
+ _isShown() {
+ return this._menu.classList.contains($i);
+ }
+ _getPlacement() {
+ const t = this._parent;
+ if (t.classList.contains("dropend")) return Wi;
+ if (t.classList.contains("dropstart")) return Bi;
+ if (t.classList.contains("dropup-center")) return "top";
+ if (t.classList.contains("dropdown-center")) return "bottom";
+ const e =
+ "end" ===
+ getComputedStyle(this._menu)
+ .getPropertyValue("--bs-position")
+ .trim();
+ return t.classList.contains("dropup") ? (e ? ji : Mi) : e ? Hi : Fi;
+ }
+ _detectNavbar() {
+ return null !== this._element.closest(".navbar");
+ }
+ _getOffset() {
+ const { offset: t } = this._config;
+ return "string" == typeof t
+ ? t.split(",").map((t) => Number.parseInt(t, 10))
+ : "function" == typeof t
+ ? (e) => t(e, this._element)
+ : t;
+ }
+ _getPopperConfig() {
+ const t = {
+ placement: this._getPlacement(),
+ modifiers: [
+ {
+ name: "preventOverflow",
+ options: { boundary: this._config.boundary },
+ },
+ { name: "offset", options: { offset: this._getOffset() } },
+ ],
+ };
+ return (
+ (this._inNavbar || "static" === this._config.display) &&
+ (F.setDataAttribute(this._menu, "popper", "static"),
+ (t.modifiers = [{ name: "applyStyles", enabled: !1 }])),
+ { ...t, ...g(this._config.popperConfig, [t]) }
+ );
+ }
+ _selectMenuItem({ key: t, target: e }) {
+ const i = z
+ .find(
+ ".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",
+ this._menu
+ )
+ .filter((t) => a(t));
+ i.length && b(i, e, t === Ti, !i.includes(e)).focus();
+ }
+ static jQueryInterface(t) {
+ return this.each(function () {
+ const e = qi.getOrCreateInstance(this, t);
+ if ("string" == typeof t) {
+ if (void 0 === e[t])
+ throw new TypeError(`No method named "${t}"`);
+ e[t]();
+ }
+ });
+ }
+ static clearMenus(t) {
+ if (2 === t.button || ("keyup" === t.type && "Tab" !== t.key))
+ return;
+ const e = z.find(Ni);
+ for (const i of e) {
+ const e = qi.getInstance(i);
+ if (!e || !1 === e._config.autoClose) continue;
+ const n = t.composedPath(),
+ s = n.includes(e._menu);
+ if (
+ n.includes(e._element) ||
+ ("inside" === e._config.autoClose && !s) ||
+ ("outside" === e._config.autoClose && s)
+ )
+ continue;
+ if (
+ e._menu.contains(t.target) &&
+ (("keyup" === t.type && "Tab" === t.key) ||
+ /input|select|option|textarea|form/i.test(
+ t.target.tagName
+ ))
+ )
+ continue;
+ const o = { relatedTarget: e._element };
+ "click" === t.type && (o.clickEvent = t), e._completeHide(o);
+ }
+ }
+ static dataApiKeydownHandler(t) {
+ const e = /input|textarea/i.test(t.target.tagName),
+ i = "Escape" === t.key,
+ n = [Ei, Ti].includes(t.key);
+ if (!n && !i) return;
+ if (e && !i) return;
+ t.preventDefault();
+ const s = this.matches(Ii)
+ ? this
+ : z.prev(this, Ii)[0] ||
+ z.next(this, Ii)[0] ||
+ z.findOne(Ii, t.delegateTarget.parentNode),
+ o = qi.getOrCreateInstance(s);
+ if (n)
+ return t.stopPropagation(), o.show(), void o._selectMenuItem(t);
+ o._isShown() && (t.stopPropagation(), o.hide(), s.focus());
+ }
+ }
+ N.on(document, Si, Ii, qi.dataApiKeydownHandler),
+ N.on(document, Si, Pi, qi.dataApiKeydownHandler),
+ N.on(document, Li, qi.clearMenus),
+ N.on(document, Di, qi.clearMenus),
+ N.on(document, Li, Ii, function (t) {
+ t.preventDefault(), qi.getOrCreateInstance(this).toggle();
+ }),
+ m(qi);
+ const Vi = "backdrop",
+ Ki = "show",
+ Qi = `mousedown.bs.${Vi}`,
+ Xi = {
+ className: "modal-backdrop",
+ clickCallback: null,
+ isAnimated: !1,
+ isVisible: !0,
+ rootElement: "body",
+ },
+ Yi = {
+ className: "string",
+ clickCallback: "(function|null)",
+ isAnimated: "boolean",
+ isVisible: "boolean",
+ rootElement: "(element|string)",
+ };
+ class Ui extends H {
+ constructor(t) {
+ super(),
+ (this._config = this._getConfig(t)),
+ (this._isAppended = !1),
+ (this._element = null);
+ }
+ static get Default() {
+ return Xi;
+ }
+ static get DefaultType() {
+ return Yi;
+ }
+ static get NAME() {
+ return Vi;
+ }
+ show(t) {
+ if (!this._config.isVisible) return void g(t);
+ this._append();
+ const e = this._getElement();
+ this._config.isAnimated && d(e),
+ e.classList.add(Ki),
+ this._emulateAnimation(() => {
+ g(t);
+ });
+ }
+ hide(t) {
+ this._config.isVisible
+ ? (this._getElement().classList.remove(Ki),
+ this._emulateAnimation(() => {
+ this.dispose(), g(t);
+ }))
+ : g(t);
+ }
+ dispose() {
+ this._isAppended &&
+ (N.off(this._element, Qi),
+ this._element.remove(),
+ (this._isAppended = !1));
+ }
+ _getElement() {
+ if (!this._element) {
+ const t = document.createElement("div");
+ (t.className = this._config.className),
+ this._config.isAnimated && t.classList.add("fade"),
+ (this._element = t);
+ }
+ return this._element;
+ }
+ _configAfterMerge(t) {
+ return (t.rootElement = r(t.rootElement)), t;
+ }
+ _append() {
+ if (this._isAppended) return;
+ const t = this._getElement();
+ this._config.rootElement.append(t),
+ N.on(t, Qi, () => {
+ g(this._config.clickCallback);
+ }),
+ (this._isAppended = !0);
+ }
+ _emulateAnimation(t) {
+ _(t, this._getElement(), this._config.isAnimated);
+ }
+ }
+ const Gi = ".bs.focustrap",
+ Ji = `focusin${Gi}`,
+ Zi = `keydown.tab${Gi}`,
+ tn = "backward",
+ en = { autofocus: !0, trapElement: null },
+ nn = { autofocus: "boolean", trapElement: "element" };
+ class sn extends H {
+ constructor(t) {
+ super(),
+ (this._config = this._getConfig(t)),
+ (this._isActive = !1),
+ (this._lastTabNavDirection = null);
+ }
+ static get Default() {
+ return en;
+ }
+ static get DefaultType() {
+ return nn;
+ }
+ static get NAME() {
+ return "focustrap";
+ }
+ activate() {
+ this._isActive ||
+ (this._config.autofocus && this._config.trapElement.focus(),
+ N.off(document, Gi),
+ N.on(document, Ji, (t) => this._handleFocusin(t)),
+ N.on(document, Zi, (t) => this._handleKeydown(t)),
+ (this._isActive = !0));
+ }
+ deactivate() {
+ this._isActive && ((this._isActive = !1), N.off(document, Gi));
+ }
+ _handleFocusin(t) {
+ const { trapElement: e } = this._config;
+ if (t.target === document || t.target === e || e.contains(t.target))
+ return;
+ const i = z.focusableChildren(e);
+ 0 === i.length
+ ? e.focus()
+ : this._lastTabNavDirection === tn
+ ? i[i.length - 1].focus()
+ : i[0].focus();
+ }
+ _handleKeydown(t) {
+ "Tab" === t.key &&
+ (this._lastTabNavDirection = t.shiftKey ? tn : "forward");
+ }
+ }
+ const on = ".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",
+ rn = ".sticky-top",
+ an = "padding-right",
+ ln = "margin-right";
+ class cn {
+ constructor() {
+ this._element = document.body;
+ }
+ getWidth() {
+ const t = document.documentElement.clientWidth;
+ return Math.abs(window.innerWidth - t);
+ }
+ hide() {
+ const t = this.getWidth();
+ this._disableOverFlow(),
+ this._setElementAttributes(this._element, an, (e) => e + t),
+ this._setElementAttributes(on, an, (e) => e + t),
+ this._setElementAttributes(rn, ln, (e) => e - t);
+ }
+ reset() {
+ this._resetElementAttributes(this._element, "overflow"),
+ this._resetElementAttributes(this._element, an),
+ this._resetElementAttributes(on, an),
+ this._resetElementAttributes(rn, ln);
+ }
+ isOverflowing() {
+ return this.getWidth() > 0;
+ }
+ _disableOverFlow() {
+ this._saveInitialAttribute(this._element, "overflow"),
+ (this._element.style.overflow = "hidden");
+ }
+ _setElementAttributes(t, e, i) {
+ const n = this.getWidth();
+ this._applyManipulationCallback(t, (t) => {
+ if (
+ t !== this._element &&
+ window.innerWidth > t.clientWidth + n
+ )
+ return;
+ this._saveInitialAttribute(t, e);
+ const s = window.getComputedStyle(t).getPropertyValue(e);
+ t.style.setProperty(e, `${i(Number.parseFloat(s))}px`);
+ });
+ }
+ _saveInitialAttribute(t, e) {
+ const i = t.style.getPropertyValue(e);
+ i && F.setDataAttribute(t, e, i);
+ }
+ _resetElementAttributes(t, e) {
+ this._applyManipulationCallback(t, (t) => {
+ const i = F.getDataAttribute(t, e);
+ null !== i
+ ? (F.removeDataAttribute(t, e), t.style.setProperty(e, i))
+ : t.style.removeProperty(e);
+ });
+ }
+ _applyManipulationCallback(t, e) {
+ if (o(t)) e(t);
+ else for (const i of z.find(t, this._element)) e(i);
+ }
+ }
+ const hn = ".bs.modal",
+ dn = `hide${hn}`,
+ un = `hidePrevented${hn}`,
+ fn = `hidden${hn}`,
+ pn = `show${hn}`,
+ mn = `shown${hn}`,
+ gn = `resize${hn}`,
+ _n = `click.dismiss${hn}`,
+ bn = `mousedown.dismiss${hn}`,
+ vn = `keydown.dismiss${hn}`,
+ yn = `click${hn}.data-api`,
+ wn = "modal-open",
+ An = "show",
+ En = "modal-static",
+ Tn = { backdrop: !0, focus: !0, keyboard: !0 },
+ Cn = {
+ backdrop: "(boolean|string)",
+ focus: "boolean",
+ keyboard: "boolean",
+ };
+ class On extends W {
+ constructor(t, e) {
+ super(t, e),
+ (this._dialog = z.findOne(".modal-dialog", this._element)),
+ (this._backdrop = this._initializeBackDrop()),
+ (this._focustrap = this._initializeFocusTrap()),
+ (this._isShown = !1),
+ (this._isTransitioning = !1),
+ (this._scrollBar = new cn()),
+ this._addEventListeners();
+ }
+ static get Default() {
+ return Tn;
+ }
+ static get DefaultType() {
+ return Cn;
+ }
+ static get NAME() {
+ return "modal";
+ }
+ toggle(t) {
+ return this._isShown ? this.hide() : this.show(t);
+ }
+ show(t) {
+ this._isShown ||
+ this._isTransitioning ||
+ N.trigger(this._element, pn, { relatedTarget: t })
+ .defaultPrevented ||
+ ((this._isShown = !0),
+ (this._isTransitioning = !0),
+ this._scrollBar.hide(),
+ document.body.classList.add(wn),
+ this._adjustDialog(),
+ this._backdrop.show(() => this._showElement(t)));
+ }
+ hide() {
+ this._isShown &&
+ !this._isTransitioning &&
+ (N.trigger(this._element, dn).defaultPrevented ||
+ ((this._isShown = !1),
+ (this._isTransitioning = !0),
+ this._focustrap.deactivate(),
+ this._element.classList.remove(An),
+ this._queueCallback(
+ () => this._hideModal(),
+ this._element,
+ this._isAnimated()
+ )));
+ }
+ dispose() {
+ N.off(window, hn),
+ N.off(this._dialog, hn),
+ this._backdrop.dispose(),
+ this._focustrap.deactivate(),
+ super.dispose();
+ }
+ handleUpdate() {
+ this._adjustDialog();
+ }
+ _initializeBackDrop() {
+ return new Ui({
+ isVisible: Boolean(this._config.backdrop),
+ isAnimated: this._isAnimated(),
+ });
+ }
+ _initializeFocusTrap() {
+ return new sn({ trapElement: this._element });
+ }
+ _showElement(t) {
+ document.body.contains(this._element) ||
+ document.body.append(this._element),
+ (this._element.style.display = "block"),
+ this._element.removeAttribute("aria-hidden"),
+ this._element.setAttribute("aria-modal", !0),
+ this._element.setAttribute("role", "dialog"),
+ (this._element.scrollTop = 0);
+ const e = z.findOne(".modal-body", this._dialog);
+ e && (e.scrollTop = 0),
+ d(this._element),
+ this._element.classList.add(An),
+ this._queueCallback(
+ () => {
+ this._config.focus && this._focustrap.activate(),
+ (this._isTransitioning = !1),
+ N.trigger(this._element, mn, { relatedTarget: t });
+ },
+ this._dialog,
+ this._isAnimated()
+ );
+ }
+ _addEventListeners() {
+ N.on(this._element, vn, (t) => {
+ "Escape" === t.key &&
+ (this._config.keyboard
+ ? this.hide()
+ : this._triggerBackdropTransition());
+ }),
+ N.on(window, gn, () => {
+ this._isShown &&
+ !this._isTransitioning &&
+ this._adjustDialog();
+ }),
+ N.on(this._element, bn, (t) => {
+ N.one(this._element, _n, (e) => {
+ this._element === t.target &&
+ this._element === e.target &&
+ ("static" !== this._config.backdrop
+ ? this._config.backdrop && this.hide()
+ : this._triggerBackdropTransition());
+ });
+ });
+ }
+ _hideModal() {
+ (this._element.style.display = "none"),
+ this._element.setAttribute("aria-hidden", !0),
+ this._element.removeAttribute("aria-modal"),
+ this._element.removeAttribute("role"),
+ (this._isTransitioning = !1),
+ this._backdrop.hide(() => {
+ document.body.classList.remove(wn),
+ this._resetAdjustments(),
+ this._scrollBar.reset(),
+ N.trigger(this._element, fn);
+ });
+ }
+ _isAnimated() {
+ return this._element.classList.contains("fade");
+ }
+ _triggerBackdropTransition() {
+ if (N.trigger(this._element, un).defaultPrevented) return;
+ const t =
+ this._element.scrollHeight >
+ document.documentElement.clientHeight,
+ e = this._element.style.overflowY;
+ "hidden" === e ||
+ this._element.classList.contains(En) ||
+ (t || (this._element.style.overflowY = "hidden"),
+ this._element.classList.add(En),
+ this._queueCallback(() => {
+ this._element.classList.remove(En),
+ this._queueCallback(() => {
+ this._element.style.overflowY = e;
+ }, this._dialog);
+ }, this._dialog),
+ this._element.focus());
+ }
+ _adjustDialog() {
+ const t =
+ this._element.scrollHeight >
+ document.documentElement.clientHeight,
+ e = this._scrollBar.getWidth(),
+ i = e > 0;
+ if (i && !t) {
+ const t = p() ? "paddingLeft" : "paddingRight";
+ this._element.style[t] = `${e}px`;
+ }
+ if (!i && t) {
+ const t = p() ? "paddingRight" : "paddingLeft";
+ this._element.style[t] = `${e}px`;
+ }
+ }
+ _resetAdjustments() {
+ (this._element.style.paddingLeft = ""),
+ (this._element.style.paddingRight = "");
+ }
+ static jQueryInterface(t, e) {
+ return this.each(function () {
+ const i = On.getOrCreateInstance(this, t);
+ if ("string" == typeof t) {
+ if (void 0 === i[t])
+ throw new TypeError(`No method named "${t}"`);
+ i[t](e);
+ }
+ });
+ }
+ }
+ N.on(document, yn, '[data-bs-toggle="modal"]', function (t) {
+ const e = z.getElementFromSelector(this);
+ ["A", "AREA"].includes(this.tagName) && t.preventDefault(),
+ N.one(e, pn, (t) => {
+ t.defaultPrevented ||
+ N.one(e, fn, () => {
+ a(this) && this.focus();
+ });
+ });
+ const i = z.findOne(".modal.show");
+ i && On.getInstance(i).hide(), On.getOrCreateInstance(e).toggle(this);
+ }),
+ R(On),
+ m(On);
+ const xn = ".bs.offcanvas",
+ kn = ".data-api",
+ Ln = `load${xn}${kn}`,
+ Sn = "show",
+ Dn = "showing",
+ $n = "hiding",
+ In = ".offcanvas.show",
+ Nn = `show${xn}`,
+ Pn = `shown${xn}`,
+ Mn = `hide${xn}`,
+ jn = `hidePrevented${xn}`,
+ Fn = `hidden${xn}`,
+ Hn = `resize${xn}`,
+ Wn = `click${xn}${kn}`,
+ Bn = `keydown.dismiss${xn}`,
+ zn = { backdrop: !0, keyboard: !0, scroll: !1 },
+ Rn = {
+ backdrop: "(boolean|string)",
+ keyboard: "boolean",
+ scroll: "boolean",
+ };
+ class qn extends W {
+ constructor(t, e) {
+ super(t, e),
+ (this._isShown = !1),
+ (this._backdrop = this._initializeBackDrop()),
+ (this._focustrap = this._initializeFocusTrap()),
+ this._addEventListeners();
+ }
+ static get Default() {
+ return zn;
+ }
+ static get DefaultType() {
+ return Rn;
+ }
+ static get NAME() {
+ return "offcanvas";
+ }
+ toggle(t) {
+ return this._isShown ? this.hide() : this.show(t);
+ }
+ show(t) {
+ this._isShown ||
+ N.trigger(this._element, Nn, { relatedTarget: t })
+ .defaultPrevented ||
+ ((this._isShown = !0),
+ this._backdrop.show(),
+ this._config.scroll || new cn().hide(),
+ this._element.setAttribute("aria-modal", !0),
+ this._element.setAttribute("role", "dialog"),
+ this._element.classList.add(Dn),
+ this._queueCallback(
+ () => {
+ (this._config.scroll && !this._config.backdrop) ||
+ this._focustrap.activate(),
+ this._element.classList.add(Sn),
+ this._element.classList.remove(Dn),
+ N.trigger(this._element, Pn, { relatedTarget: t });
+ },
+ this._element,
+ !0
+ ));
+ }
+ hide() {
+ this._isShown &&
+ (N.trigger(this._element, Mn).defaultPrevented ||
+ (this._focustrap.deactivate(),
+ this._element.blur(),
+ (this._isShown = !1),
+ this._element.classList.add($n),
+ this._backdrop.hide(),
+ this._queueCallback(
+ () => {
+ this._element.classList.remove(Sn, $n),
+ this._element.removeAttribute("aria-modal"),
+ this._element.removeAttribute("role"),
+ this._config.scroll || new cn().reset(),
+ N.trigger(this._element, Fn);
+ },
+ this._element,
+ !0
+ )));
+ }
+ dispose() {
+ this._backdrop.dispose(),
+ this._focustrap.deactivate(),
+ super.dispose();
+ }
+ _initializeBackDrop() {
+ const t = Boolean(this._config.backdrop);
+ return new Ui({
+ className: "offcanvas-backdrop",
+ isVisible: t,
+ isAnimated: !0,
+ rootElement: this._element.parentNode,
+ clickCallback: t
+ ? () => {
+ "static" !== this._config.backdrop
+ ? this.hide()
+ : N.trigger(this._element, jn);
+ }
+ : null,
+ });
+ }
+ _initializeFocusTrap() {
+ return new sn({ trapElement: this._element });
+ }
+ _addEventListeners() {
+ N.on(this._element, Bn, (t) => {
+ "Escape" === t.key &&
+ (this._config.keyboard
+ ? this.hide()
+ : N.trigger(this._element, jn));
+ });
+ }
+ static jQueryInterface(t) {
+ return this.each(function () {
+ const e = qn.getOrCreateInstance(this, t);
+ if ("string" == typeof t) {
+ if (
+ void 0 === e[t] ||
+ t.startsWith("_") ||
+ "constructor" === t
+ )
+ throw new TypeError(`No method named "${t}"`);
+ e[t](this);
+ }
+ });
+ }
+ }
+ N.on(document, Wn, '[data-bs-toggle="offcanvas"]', function (t) {
+ const e = z.getElementFromSelector(this);
+ if (
+ (["A", "AREA"].includes(this.tagName) && t.preventDefault(),
+ l(this))
+ )
+ return;
+ N.one(e, Fn, () => {
+ a(this) && this.focus();
+ });
+ const i = z.findOne(In);
+ i && i !== e && qn.getInstance(i).hide(),
+ qn.getOrCreateInstance(e).toggle(this);
+ }),
+ N.on(window, Ln, () => {
+ for (const t of z.find(In)) qn.getOrCreateInstance(t).show();
+ }),
+ N.on(window, Hn, () => {
+ for (const t of z.find(
+ "[aria-modal][class*=show][class*=offcanvas-]"
+ ))
+ "fixed" !== getComputedStyle(t).position &&
+ qn.getOrCreateInstance(t).hide();
+ }),
+ R(qn),
+ m(qn);
+ const Vn = {
+ "*": ["class", "dir", "id", "lang", "role", /^aria-[\w-]*$/i],
+ a: ["target", "href", "title", "rel"],
+ area: [],
+ b: [],
+ br: [],
+ col: [],
+ code: [],
+ div: [],
+ em: [],
+ hr: [],
+ h1: [],
+ h2: [],
+ h3: [],
+ h4: [],
+ h5: [],
+ h6: [],
+ i: [],
+ img: ["src", "srcset", "alt", "title", "width", "height"],
+ li: [],
+ ol: [],
+ p: [],
+ pre: [],
+ s: [],
+ small: [],
+ span: [],
+ sub: [],
+ sup: [],
+ strong: [],
+ u: [],
+ ul: [],
+ },
+ Kn = new Set([
+ "background",
+ "cite",
+ "href",
+ "itemtype",
+ "longdesc",
+ "poster",
+ "src",
+ "xlink:href",
+ ]),
+ Qn = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,
+ Xn = (t, e) => {
+ const i = t.nodeName.toLowerCase();
+ return e.includes(i)
+ ? !Kn.has(i) || Boolean(Qn.test(t.nodeValue))
+ : e.filter((t) => t instanceof RegExp).some((t) => t.test(i));
+ },
+ Yn = {
+ allowList: Vn,
+ content: {},
+ extraClass: "",
+ html: !1,
+ sanitize: !0,
+ sanitizeFn: null,
+ template: "",
+ },
+ Un = {
+ allowList: "object",
+ content: "object",
+ extraClass: "(string|function)",
+ html: "boolean",
+ sanitize: "boolean",
+ sanitizeFn: "(null|function)",
+ template: "string",
+ },
+ Gn = {
+ entry: "(string|element|function|null)",
+ selector: "(string|element)",
+ };
+ class Jn extends H {
+ constructor(t) {
+ super(), (this._config = this._getConfig(t));
+ }
+ static get Default() {
+ return Yn;
+ }
+ static get DefaultType() {
+ return Un;
+ }
+ static get NAME() {
+ return "TemplateFactory";
+ }
+ getContent() {
+ return Object.values(this._config.content)
+ .map((t) => this._resolvePossibleFunction(t))
+ .filter(Boolean);
+ }
+ hasContent() {
+ return this.getContent().length > 0;
+ }
+ changeContent(t) {
+ return (
+ this._checkContent(t),
+ (this._config.content = { ...this._config.content, ...t }),
+ this
+ );
+ }
+ toHtml() {
+ const t = document.createElement("div");
+ t.innerHTML = this._maybeSanitize(this._config.template);
+ for (const [e, i] of Object.entries(this._config.content))
+ this._setContent(t, i, e);
+ const e = t.children[0],
+ i = this._resolvePossibleFunction(this._config.extraClass);
+ return i && e.classList.add(...i.split(" ")), e;
+ }
+ _typeCheckConfig(t) {
+ super._typeCheckConfig(t), this._checkContent(t.content);
+ }
+ _checkContent(t) {
+ for (const [e, i] of Object.entries(t))
+ super._typeCheckConfig({ selector: e, entry: i }, Gn);
+ }
+ _setContent(t, e, i) {
+ const n = z.findOne(i, t);
+ n &&
+ ((e = this._resolvePossibleFunction(e))
+ ? o(e)
+ ? this._putElementInTemplate(r(e), n)
+ : this._config.html
+ ? (n.innerHTML = this._maybeSanitize(e))
+ : (n.textContent = e)
+ : n.remove());
+ }
+ _maybeSanitize(t) {
+ return this._config.sanitize
+ ? (function (t, e, i) {
+ if (!t.length) return t;
+ if (i && "function" == typeof i) return i(t);
+ const n = new window.DOMParser().parseFromString(
+ t,
+ "text/html"
+ ),
+ s = [].concat(...n.body.querySelectorAll("*"));
+ for (const t of s) {
+ const i = t.nodeName.toLowerCase();
+ if (!Object.keys(e).includes(i)) {
+ t.remove();
+ continue;
+ }
+ const n = [].concat(...t.attributes),
+ s = [].concat(e["*"] || [], e[i] || []);
+ for (const e of n)
+ Xn(e, s) || t.removeAttribute(e.nodeName);
+ }
+ return n.body.innerHTML;
+ })(t, this._config.allowList, this._config.sanitizeFn)
+ : t;
+ }
+ _resolvePossibleFunction(t) {
+ return g(t, [this]);
+ }
+ _putElementInTemplate(t, e) {
+ if (this._config.html) return (e.innerHTML = ""), void e.append(t);
+ e.textContent = t.textContent;
+ }
+ }
+ const Zn = new Set(["sanitize", "allowList", "sanitizeFn"]),
+ ts = "fade",
+ es = "show",
+ is = ".modal",
+ ns = "hide.bs.modal",
+ ss = "hover",
+ os = "focus",
+ rs = {
+ AUTO: "auto",
+ TOP: "top",
+ RIGHT: p() ? "left" : "right",
+ BOTTOM: "bottom",
+ LEFT: p() ? "right" : "left",
+ },
+ as = {
+ allowList: Vn,
+ animation: !0,
+ boundary: "clippingParents",
+ container: !1,
+ customClass: "",
+ delay: 0,
+ fallbackPlacements: ["top", "right", "bottom", "left"],
+ html: !1,
+ offset: [0, 6],
+ placement: "top",
+ popperConfig: null,
+ sanitize: !0,
+ sanitizeFn: null,
+ selector: !1,
+ template:
+ '',
+ title: "",
+ trigger: "hover focus",
+ },
+ ls = {
+ allowList: "object",
+ animation: "boolean",
+ boundary: "(string|element)",
+ container: "(string|element|boolean)",
+ customClass: "(string|function)",
+ delay: "(number|object)",
+ fallbackPlacements: "array",
+ html: "boolean",
+ offset: "(array|string|function)",
+ placement: "(string|function)",
+ popperConfig: "(null|object|function)",
+ sanitize: "boolean",
+ sanitizeFn: "(null|function)",
+ selector: "(string|boolean)",
+ template: "string",
+ title: "(string|element|function)",
+ trigger: "string",
+ };
+ class cs extends W {
+ constructor(t, e) {
+ if (void 0 === vi)
+ throw new TypeError(
+ "Bootstrap's tooltips require Popper (https://popper.js.org)"
+ );
+ super(t, e),
+ (this._isEnabled = !0),
+ (this._timeout = 0),
+ (this._isHovered = null),
+ (this._activeTrigger = {}),
+ (this._popper = null),
+ (this._templateFactory = null),
+ (this._newContent = null),
+ (this.tip = null),
+ this._setListeners(),
+ this._config.selector || this._fixTitle();
+ }
+ static get Default() {
+ return as;
+ }
+ static get DefaultType() {
+ return ls;
+ }
+ static get NAME() {
+ return "tooltip";
+ }
+ enable() {
+ this._isEnabled = !0;
+ }
+ disable() {
+ this._isEnabled = !1;
+ }
+ toggleEnabled() {
+ this._isEnabled = !this._isEnabled;
+ }
+ toggle() {
+ this._isEnabled &&
+ ((this._activeTrigger.click = !this._activeTrigger.click),
+ this._isShown() ? this._leave() : this._enter());
+ }
+ dispose() {
+ clearTimeout(this._timeout),
+ N.off(this._element.closest(is), ns, this._hideModalHandler),
+ this._element.getAttribute("data-bs-original-title") &&
+ this._element.setAttribute(
+ "title",
+ this._element.getAttribute("data-bs-original-title")
+ ),
+ this._disposePopper(),
+ super.dispose();
+ }
+ show() {
+ if ("none" === this._element.style.display)
+ throw new Error("Please use show on visible elements");
+ if (!this._isWithContent() || !this._isEnabled) return;
+ const t = N.trigger(
+ this._element,
+ this.constructor.eventName("show")
+ ),
+ e = (
+ c(this._element) ||
+ this._element.ownerDocument.documentElement
+ ).contains(this._element);
+ if (t.defaultPrevented || !e) return;
+ this._disposePopper();
+ const i = this._getTipElement();
+ this._element.setAttribute(
+ "aria-describedby",
+ i.getAttribute("id")
+ );
+ const { container: n } = this._config;
+ if (
+ (this._element.ownerDocument.documentElement.contains(
+ this.tip
+ ) ||
+ (n.append(i),
+ N.trigger(
+ this._element,
+ this.constructor.eventName("inserted")
+ )),
+ (this._popper = this._createPopper(i)),
+ i.classList.add(es),
+ "ontouchstart" in document.documentElement)
+ )
+ for (const t of [].concat(...document.body.children))
+ N.on(t, "mouseover", h);
+ this._queueCallback(
+ () => {
+ N.trigger(
+ this._element,
+ this.constructor.eventName("shown")
+ ),
+ !1 === this._isHovered && this._leave(),
+ (this._isHovered = !1);
+ },
+ this.tip,
+ this._isAnimated()
+ );
+ }
+ hide() {
+ if (
+ this._isShown() &&
+ !N.trigger(this._element, this.constructor.eventName("hide"))
+ .defaultPrevented
+ ) {
+ if (
+ (this._getTipElement().classList.remove(es),
+ "ontouchstart" in document.documentElement)
+ )
+ for (const t of [].concat(...document.body.children))
+ N.off(t, "mouseover", h);
+ (this._activeTrigger.click = !1),
+ (this._activeTrigger[os] = !1),
+ (this._activeTrigger[ss] = !1),
+ (this._isHovered = null),
+ this._queueCallback(
+ () => {
+ this._isWithActiveTrigger() ||
+ (this._isHovered || this._disposePopper(),
+ this._element.removeAttribute(
+ "aria-describedby"
+ ),
+ N.trigger(
+ this._element,
+ this.constructor.eventName("hidden")
+ ));
+ },
+ this.tip,
+ this._isAnimated()
+ );
+ }
+ }
+ update() {
+ this._popper && this._popper.update();
+ }
+ _isWithContent() {
+ return Boolean(this._getTitle());
+ }
+ _getTipElement() {
+ return (
+ this.tip ||
+ (this.tip = this._createTipElement(
+ this._newContent || this._getContentForTemplate()
+ )),
+ this.tip
+ );
+ }
+ _createTipElement(t) {
+ const e = this._getTemplateFactory(t).toHtml();
+ if (!e) return null;
+ e.classList.remove(ts, es),
+ e.classList.add(`bs-${this.constructor.NAME}-auto`);
+ const i = ((t) => {
+ do {
+ t += Math.floor(1e6 * Math.random());
+ } while (document.getElementById(t));
+ return t;
+ })(this.constructor.NAME).toString();
+ return (
+ e.setAttribute("id", i),
+ this._isAnimated() && e.classList.add(ts),
+ e
+ );
+ }
+ setContent(t) {
+ (this._newContent = t),
+ this._isShown() && (this._disposePopper(), this.show());
+ }
+ _getTemplateFactory(t) {
+ return (
+ this._templateFactory
+ ? this._templateFactory.changeContent(t)
+ : (this._templateFactory = new Jn({
+ ...this._config,
+ content: t,
+ extraClass: this._resolvePossibleFunction(
+ this._config.customClass
+ ),
+ })),
+ this._templateFactory
+ );
+ }
+ _getContentForTemplate() {
+ return { ".tooltip-inner": this._getTitle() };
+ }
+ _getTitle() {
+ return (
+ this._resolvePossibleFunction(this._config.title) ||
+ this._element.getAttribute("data-bs-original-title")
+ );
+ }
+ _initializeOnDelegatedTarget(t) {
+ return this.constructor.getOrCreateInstance(
+ t.delegateTarget,
+ this._getDelegateConfig()
+ );
+ }
+ _isAnimated() {
+ return (
+ this._config.animation ||
+ (this.tip && this.tip.classList.contains(ts))
+ );
+ }
+ _isShown() {
+ return this.tip && this.tip.classList.contains(es);
+ }
+ _createPopper(t) {
+ const e = g(this._config.placement, [this, t, this._element]),
+ i = rs[e.toUpperCase()];
+ return bi(this._element, t, this._getPopperConfig(i));
+ }
+ _getOffset() {
+ const { offset: t } = this._config;
+ return "string" == typeof t
+ ? t.split(",").map((t) => Number.parseInt(t, 10))
+ : "function" == typeof t
+ ? (e) => t(e, this._element)
+ : t;
+ }
+ _resolvePossibleFunction(t) {
+ return g(t, [this._element]);
+ }
+ _getPopperConfig(t) {
+ const e = {
+ placement: t,
+ modifiers: [
+ {
+ name: "flip",
+ options: {
+ fallbackPlacements: this._config.fallbackPlacements,
+ },
+ },
+ { name: "offset", options: { offset: this._getOffset() } },
+ {
+ name: "preventOverflow",
+ options: { boundary: this._config.boundary },
+ },
+ {
+ name: "arrow",
+ options: { element: `.${this.constructor.NAME}-arrow` },
+ },
+ {
+ name: "preSetPlacement",
+ enabled: !0,
+ phase: "beforeMain",
+ fn: (t) => {
+ this._getTipElement().setAttribute(
+ "data-popper-placement",
+ t.state.placement
+ );
+ },
+ },
+ ],
+ };
+ return { ...e, ...g(this._config.popperConfig, [e]) };
+ }
+ _setListeners() {
+ const t = this._config.trigger.split(" ");
+ for (const e of t)
+ if ("click" === e)
+ N.on(
+ this._element,
+ this.constructor.eventName("click"),
+ this._config.selector,
+ (t) => {
+ this._initializeOnDelegatedTarget(t).toggle();
+ }
+ );
+ else if ("manual" !== e) {
+ const t =
+ e === ss
+ ? this.constructor.eventName("mouseenter")
+ : this.constructor.eventName("focusin"),
+ i =
+ e === ss
+ ? this.constructor.eventName("mouseleave")
+ : this.constructor.eventName("focusout");
+ N.on(this._element, t, this._config.selector, (t) => {
+ const e = this._initializeOnDelegatedTarget(t);
+ (e._activeTrigger["focusin" === t.type ? os : ss] = !0),
+ e._enter();
+ }),
+ N.on(this._element, i, this._config.selector, (t) => {
+ const e = this._initializeOnDelegatedTarget(t);
+ (e._activeTrigger["focusout" === t.type ? os : ss] =
+ e._element.contains(t.relatedTarget)),
+ e._leave();
+ });
+ }
+ (this._hideModalHandler = () => {
+ this._element && this.hide();
+ }),
+ N.on(this._element.closest(is), ns, this._hideModalHandler);
+ }
+ _fixTitle() {
+ const t = this._element.getAttribute("title");
+ t &&
+ (this._element.getAttribute("aria-label") ||
+ this._element.textContent.trim() ||
+ this._element.setAttribute("aria-label", t),
+ this._element.setAttribute("data-bs-original-title", t),
+ this._element.removeAttribute("title"));
+ }
+ _enter() {
+ this._isShown() || this._isHovered
+ ? (this._isHovered = !0)
+ : ((this._isHovered = !0),
+ this._setTimeout(() => {
+ this._isHovered && this.show();
+ }, this._config.delay.show));
+ }
+ _leave() {
+ this._isWithActiveTrigger() ||
+ ((this._isHovered = !1),
+ this._setTimeout(() => {
+ this._isHovered || this.hide();
+ }, this._config.delay.hide));
+ }
+ _setTimeout(t, e) {
+ clearTimeout(this._timeout), (this._timeout = setTimeout(t, e));
+ }
+ _isWithActiveTrigger() {
+ return Object.values(this._activeTrigger).includes(!0);
+ }
+ _getConfig(t) {
+ const e = F.getDataAttributes(this._element);
+ for (const t of Object.keys(e)) Zn.has(t) && delete e[t];
+ return (
+ (t = { ...e, ...("object" == typeof t && t ? t : {}) }),
+ (t = this._mergeConfigObj(t)),
+ (t = this._configAfterMerge(t)),
+ this._typeCheckConfig(t),
+ t
+ );
+ }
+ _configAfterMerge(t) {
+ return (
+ (t.container =
+ !1 === t.container ? document.body : r(t.container)),
+ "number" == typeof t.delay &&
+ (t.delay = { show: t.delay, hide: t.delay }),
+ "number" == typeof t.title && (t.title = t.title.toString()),
+ "number" == typeof t.content &&
+ (t.content = t.content.toString()),
+ t
+ );
+ }
+ _getDelegateConfig() {
+ const t = {};
+ for (const [e, i] of Object.entries(this._config))
+ this.constructor.Default[e] !== i && (t[e] = i);
+ return (t.selector = !1), (t.trigger = "manual"), t;
+ }
+ _disposePopper() {
+ this._popper && (this._popper.destroy(), (this._popper = null)),
+ this.tip && (this.tip.remove(), (this.tip = null));
+ }
+ static jQueryInterface(t) {
+ return this.each(function () {
+ const e = cs.getOrCreateInstance(this, t);
+ if ("string" == typeof t) {
+ if (void 0 === e[t])
+ throw new TypeError(`No method named "${t}"`);
+ e[t]();
+ }
+ });
+ }
+ }
+ m(cs);
+ const hs = {
+ ...cs.Default,
+ content: "",
+ offset: [0, 8],
+ placement: "right",
+ template:
+ '',
+ trigger: "click",
+ },
+ ds = { ...cs.DefaultType, content: "(null|string|element|function)" };
+ class us extends cs {
+ static get Default() {
+ return hs;
+ }
+ static get DefaultType() {
+ return ds;
+ }
+ static get NAME() {
+ return "popover";
+ }
+ _isWithContent() {
+ return this._getTitle() || this._getContent();
+ }
+ _getContentForTemplate() {
+ return {
+ ".popover-header": this._getTitle(),
+ ".popover-body": this._getContent(),
+ };
+ }
+ _getContent() {
+ return this._resolvePossibleFunction(this._config.content);
+ }
+ static jQueryInterface(t) {
+ return this.each(function () {
+ const e = us.getOrCreateInstance(this, t);
+ if ("string" == typeof t) {
+ if (void 0 === e[t])
+ throw new TypeError(`No method named "${t}"`);
+ e[t]();
+ }
+ });
+ }
+ }
+ m(us);
+ const fs = ".bs.scrollspy",
+ ps = `activate${fs}`,
+ ms = `click${fs}`,
+ gs = `load${fs}.data-api`,
+ _s = "active",
+ bs = "[href]",
+ vs = ".nav-link",
+ ys = `${vs}, .nav-item > ${vs}, .list-group-item`,
+ ws = {
+ offset: null,
+ rootMargin: "0px 0px -25%",
+ smoothScroll: !1,
+ target: null,
+ threshold: [0.1, 0.5, 1],
+ },
+ As = {
+ offset: "(number|null)",
+ rootMargin: "string",
+ smoothScroll: "boolean",
+ target: "element",
+ threshold: "array",
+ };
+ class Es extends W {
+ constructor(t, e) {
+ super(t, e),
+ (this._targetLinks = new Map()),
+ (this._observableSections = new Map()),
+ (this._rootElement =
+ "visible" === getComputedStyle(this._element).overflowY
+ ? null
+ : this._element),
+ (this._activeTarget = null),
+ (this._observer = null),
+ (this._previousScrollData = {
+ visibleEntryTop: 0,
+ parentScrollTop: 0,
+ }),
+ this.refresh();
+ }
+ static get Default() {
+ return ws;
+ }
+ static get DefaultType() {
+ return As;
+ }
+ static get NAME() {
+ return "scrollspy";
+ }
+ refresh() {
+ this._initializeTargetsAndObservables(),
+ this._maybeEnableSmoothScroll(),
+ this._observer
+ ? this._observer.disconnect()
+ : (this._observer = this._getNewObserver());
+ for (const t of this._observableSections.values())
+ this._observer.observe(t);
+ }
+ dispose() {
+ this._observer.disconnect(), super.dispose();
+ }
+ _configAfterMerge(t) {
+ return (
+ (t.target = r(t.target) || document.body),
+ (t.rootMargin = t.offset
+ ? `${t.offset}px 0px -30%`
+ : t.rootMargin),
+ "string" == typeof t.threshold &&
+ (t.threshold = t.threshold
+ .split(",")
+ .map((t) => Number.parseFloat(t))),
+ t
+ );
+ }
+ _maybeEnableSmoothScroll() {
+ this._config.smoothScroll &&
+ (N.off(this._config.target, ms),
+ N.on(this._config.target, ms, bs, (t) => {
+ const e = this._observableSections.get(t.target.hash);
+ if (e) {
+ t.preventDefault();
+ const i = this._rootElement || window,
+ n = e.offsetTop - this._element.offsetTop;
+ if (i.scrollTo)
+ return void i.scrollTo({
+ top: n,
+ behavior: "smooth",
+ });
+ i.scrollTop = n;
+ }
+ }));
+ }
+ _getNewObserver() {
+ const t = {
+ root: this._rootElement,
+ threshold: this._config.threshold,
+ rootMargin: this._config.rootMargin,
+ };
+ return new IntersectionObserver(
+ (t) => this._observerCallback(t),
+ t
+ );
+ }
+ _observerCallback(t) {
+ const e = (t) => this._targetLinks.get(`#${t.target.id}`),
+ i = (t) => {
+ (this._previousScrollData.visibleEntryTop =
+ t.target.offsetTop),
+ this._process(e(t));
+ },
+ n = (this._rootElement || document.documentElement).scrollTop,
+ s = n >= this._previousScrollData.parentScrollTop;
+ this._previousScrollData.parentScrollTop = n;
+ for (const o of t) {
+ if (!o.isIntersecting) {
+ (this._activeTarget = null), this._clearActiveClass(e(o));
+ continue;
+ }
+ const t =
+ o.target.offsetTop >=
+ this._previousScrollData.visibleEntryTop;
+ if (s && t) {
+ if ((i(o), !n)) return;
+ } else s || t || i(o);
+ }
+ }
+ _initializeTargetsAndObservables() {
+ (this._targetLinks = new Map()),
+ (this._observableSections = new Map());
+ const t = z.find(bs, this._config.target);
+ for (const e of t) {
+ if (!e.hash || l(e)) continue;
+ const t = z.findOne(decodeURI(e.hash), this._element);
+ a(t) &&
+ (this._targetLinks.set(decodeURI(e.hash), e),
+ this._observableSections.set(e.hash, t));
+ }
+ }
+ _process(t) {
+ this._activeTarget !== t &&
+ (this._clearActiveClass(this._config.target),
+ (this._activeTarget = t),
+ t.classList.add(_s),
+ this._activateParents(t),
+ N.trigger(this._element, ps, { relatedTarget: t }));
+ }
+ _activateParents(t) {
+ if (t.classList.contains("dropdown-item"))
+ z.findOne(
+ ".dropdown-toggle",
+ t.closest(".dropdown")
+ ).classList.add(_s);
+ else
+ for (const e of z.parents(t, ".nav, .list-group"))
+ for (const t of z.prev(e, ys)) t.classList.add(_s);
+ }
+ _clearActiveClass(t) {
+ t.classList.remove(_s);
+ const e = z.find(`${bs}.${_s}`, t);
+ for (const t of e) t.classList.remove(_s);
+ }
+ static jQueryInterface(t) {
+ return this.each(function () {
+ const e = Es.getOrCreateInstance(this, t);
+ if ("string" == typeof t) {
+ if (
+ void 0 === e[t] ||
+ t.startsWith("_") ||
+ "constructor" === t
+ )
+ throw new TypeError(`No method named "${t}"`);
+ e[t]();
+ }
+ });
+ }
+ }
+ N.on(window, gs, () => {
+ for (const t of z.find('[data-bs-spy="scroll"]'))
+ Es.getOrCreateInstance(t);
+ }),
+ m(Es);
+ const Ts = ".bs.tab",
+ Cs = `hide${Ts}`,
+ Os = `hidden${Ts}`,
+ xs = `show${Ts}`,
+ ks = `shown${Ts}`,
+ Ls = `click${Ts}`,
+ Ss = `keydown${Ts}`,
+ Ds = `load${Ts}`,
+ $s = "ArrowLeft",
+ Is = "ArrowRight",
+ Ns = "ArrowUp",
+ Ps = "ArrowDown",
+ Ms = "Home",
+ js = "End",
+ Fs = "active",
+ Hs = "fade",
+ Ws = "show",
+ Bs = ".dropdown-toggle",
+ zs = `:not(${Bs})`,
+ Rs =
+ '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',
+ qs = `.nav-link${zs}, .list-group-item${zs}, [role="tab"]${zs}, ${Rs}`,
+ Vs = `.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;
+ class Ks extends W {
+ constructor(t) {
+ super(t),
+ (this._parent = this._element.closest(
+ '.list-group, .nav, [role="tablist"]'
+ )),
+ this._parent &&
+ (this._setInitialAttributes(
+ this._parent,
+ this._getChildren()
+ ),
+ N.on(this._element, Ss, (t) => this._keydown(t)));
+ }
+ static get NAME() {
+ return "tab";
+ }
+ show() {
+ const t = this._element;
+ if (this._elemIsActive(t)) return;
+ const e = this._getActiveElem(),
+ i = e ? N.trigger(e, Cs, { relatedTarget: t }) : null;
+ N.trigger(t, xs, { relatedTarget: e }).defaultPrevented ||
+ (i && i.defaultPrevented) ||
+ (this._deactivate(e, t), this._activate(t, e));
+ }
+ _activate(t, e) {
+ t &&
+ (t.classList.add(Fs),
+ this._activate(z.getElementFromSelector(t)),
+ this._queueCallback(
+ () => {
+ "tab" === t.getAttribute("role")
+ ? (t.removeAttribute("tabindex"),
+ t.setAttribute("aria-selected", !0),
+ this._toggleDropDown(t, !0),
+ N.trigger(t, ks, { relatedTarget: e }))
+ : t.classList.add(Ws);
+ },
+ t,
+ t.classList.contains(Hs)
+ ));
+ }
+ _deactivate(t, e) {
+ t &&
+ (t.classList.remove(Fs),
+ t.blur(),
+ this._deactivate(z.getElementFromSelector(t)),
+ this._queueCallback(
+ () => {
+ "tab" === t.getAttribute("role")
+ ? (t.setAttribute("aria-selected", !1),
+ t.setAttribute("tabindex", "-1"),
+ this._toggleDropDown(t, !1),
+ N.trigger(t, Os, { relatedTarget: e }))
+ : t.classList.remove(Ws);
+ },
+ t,
+ t.classList.contains(Hs)
+ ));
+ }
+ _keydown(t) {
+ if (![$s, Is, Ns, Ps, Ms, js].includes(t.key)) return;
+ t.stopPropagation(), t.preventDefault();
+ const e = this._getChildren().filter((t) => !l(t));
+ let i;
+ if ([Ms, js].includes(t.key))
+ i = e[t.key === Ms ? 0 : e.length - 1];
+ else {
+ const n = [Is, Ps].includes(t.key);
+ i = b(e, t.target, n, !0);
+ }
+ i &&
+ (i.focus({ preventScroll: !0 }),
+ Ks.getOrCreateInstance(i).show());
+ }
+ _getChildren() {
+ return z.find(qs, this._parent);
+ }
+ _getActiveElem() {
+ return (
+ this._getChildren().find((t) => this._elemIsActive(t)) || null
+ );
+ }
+ _setInitialAttributes(t, e) {
+ this._setAttributeIfNotExists(t, "role", "tablist");
+ for (const t of e) this._setInitialAttributesOnChild(t);
+ }
+ _setInitialAttributesOnChild(t) {
+ t = this._getInnerElement(t);
+ const e = this._elemIsActive(t),
+ i = this._getOuterElement(t);
+ t.setAttribute("aria-selected", e),
+ i !== t &&
+ this._setAttributeIfNotExists(i, "role", "presentation"),
+ e || t.setAttribute("tabindex", "-1"),
+ this._setAttributeIfNotExists(t, "role", "tab"),
+ this._setInitialAttributesOnTargetPanel(t);
+ }
+ _setInitialAttributesOnTargetPanel(t) {
+ const e = z.getElementFromSelector(t);
+ e &&
+ (this._setAttributeIfNotExists(e, "role", "tabpanel"),
+ t.id &&
+ this._setAttributeIfNotExists(
+ e,
+ "aria-labelledby",
+ `${t.id}`
+ ));
+ }
+ _toggleDropDown(t, e) {
+ const i = this._getOuterElement(t);
+ if (!i.classList.contains("dropdown")) return;
+ const n = (t, n) => {
+ const s = z.findOne(t, i);
+ s && s.classList.toggle(n, e);
+ };
+ n(Bs, Fs),
+ n(".dropdown-menu", Ws),
+ i.setAttribute("aria-expanded", e);
+ }
+ _setAttributeIfNotExists(t, e, i) {
+ t.hasAttribute(e) || t.setAttribute(e, i);
+ }
+ _elemIsActive(t) {
+ return t.classList.contains(Fs);
+ }
+ _getInnerElement(t) {
+ return t.matches(qs) ? t : z.findOne(qs, t);
+ }
+ _getOuterElement(t) {
+ return t.closest(".nav-item, .list-group-item") || t;
+ }
+ static jQueryInterface(t) {
+ return this.each(function () {
+ const e = Ks.getOrCreateInstance(this);
+ if ("string" == typeof t) {
+ if (
+ void 0 === e[t] ||
+ t.startsWith("_") ||
+ "constructor" === t
+ )
+ throw new TypeError(`No method named "${t}"`);
+ e[t]();
+ }
+ });
+ }
+ }
+ N.on(document, Ls, Rs, function (t) {
+ ["A", "AREA"].includes(this.tagName) && t.preventDefault(),
+ l(this) || Ks.getOrCreateInstance(this).show();
+ }),
+ N.on(window, Ds, () => {
+ for (const t of z.find(Vs)) Ks.getOrCreateInstance(t);
+ }),
+ m(Ks);
+ const Qs = ".bs.toast",
+ Xs = `mouseover${Qs}`,
+ Ys = `mouseout${Qs}`,
+ Us = `focusin${Qs}`,
+ Gs = `focusout${Qs}`,
+ Js = `hide${Qs}`,
+ Zs = `hidden${Qs}`,
+ to = `show${Qs}`,
+ eo = `shown${Qs}`,
+ io = "hide",
+ no = "show",
+ so = "showing",
+ oo = { animation: "boolean", autohide: "boolean", delay: "number" },
+ ro = { animation: !0, autohide: !0, delay: 5e3 };
+ class ao extends W {
+ constructor(t, e) {
+ super(t, e),
+ (this._timeout = null),
+ (this._hasMouseInteraction = !1),
+ (this._hasKeyboardInteraction = !1),
+ this._setListeners();
+ }
+ static get Default() {
+ return ro;
+ }
+ static get DefaultType() {
+ return oo;
+ }
+ static get NAME() {
+ return "toast";
+ }
+ show() {
+ N.trigger(this._element, to).defaultPrevented ||
+ (this._clearTimeout(),
+ this._config.animation && this._element.classList.add("fade"),
+ this._element.classList.remove(io),
+ d(this._element),
+ this._element.classList.add(no, so),
+ this._queueCallback(
+ () => {
+ this._element.classList.remove(so),
+ N.trigger(this._element, eo),
+ this._maybeScheduleHide();
+ },
+ this._element,
+ this._config.animation
+ ));
+ }
+ hide() {
+ this.isShown() &&
+ (N.trigger(this._element, Js).defaultPrevented ||
+ (this._element.classList.add(so),
+ this._queueCallback(
+ () => {
+ this._element.classList.add(io),
+ this._element.classList.remove(so, no),
+ N.trigger(this._element, Zs);
+ },
+ this._element,
+ this._config.animation
+ )));
+ }
+ dispose() {
+ this._clearTimeout(),
+ this.isShown() && this._element.classList.remove(no),
+ super.dispose();
+ }
+ isShown() {
+ return this._element.classList.contains(no);
+ }
+ _maybeScheduleHide() {
+ this._config.autohide &&
+ (this._hasMouseInteraction ||
+ this._hasKeyboardInteraction ||
+ (this._timeout = setTimeout(() => {
+ this.hide();
+ }, this._config.delay)));
+ }
+ _onInteraction(t, e) {
+ switch (t.type) {
+ case "mouseover":
+ case "mouseout":
+ this._hasMouseInteraction = e;
+ break;
+ case "focusin":
+ case "focusout":
+ this._hasKeyboardInteraction = e;
+ }
+ if (e) return void this._clearTimeout();
+ const i = t.relatedTarget;
+ this._element === i ||
+ this._element.contains(i) ||
+ this._maybeScheduleHide();
+ }
+ _setListeners() {
+ N.on(this._element, Xs, (t) => this._onInteraction(t, !0)),
+ N.on(this._element, Ys, (t) => this._onInteraction(t, !1)),
+ N.on(this._element, Us, (t) => this._onInteraction(t, !0)),
+ N.on(this._element, Gs, (t) => this._onInteraction(t, !1));
+ }
+ _clearTimeout() {
+ clearTimeout(this._timeout), (this._timeout = null);
+ }
+ static jQueryInterface(t) {
+ return this.each(function () {
+ const e = ao.getOrCreateInstance(this, t);
+ if ("string" == typeof t) {
+ if (void 0 === e[t])
+ throw new TypeError(`No method named "${t}"`);
+ e[t](this);
+ }
+ });
+ }
+ }
+ return (
+ R(ao),
+ m(ao),
+ {
+ Alert: Q,
+ Button: Y,
+ Carousel: xt,
+ Collapse: Bt,
+ Dropdown: qi,
+ Modal: On,
+ Offcanvas: qn,
+ Popover: us,
+ ScrollSpy: Es,
+ Tab: Ks,
+ Toast: ao,
+ Tooltip: cs,
+ }
+ );
+});
+//# sourceMappingURL=bootstrap.bundle.min.js.map
diff --git a/data_embed/index.html b/data_embed/index.html
new file mode 100644
index 0000000..dcf0771
--- /dev/null
+++ b/data_embed/index.html
@@ -0,0 +1,1082 @@
+
+
+
+
+
+ LoRa iGate
+
+
+
+
+
+
+
+
+
+
+
+ Please wait... Settings will be saved and LoRa will
+ reboot
+
+
+
+
+
+
+
+
+
+
diff --git a/data_embed/script.js b/data_embed/script.js
new file mode 100644
index 0000000..eed32c2
--- /dev/null
+++ b/data_embed/script.js
@@ -0,0 +1,388 @@
+// Custom scripts
+
+let currentSettings = null;
+
+function backupSettings() {
+ const data =
+ "data:text/json;charset=utf-8," +
+ encodeURIComponent(JSON.stringify(currentSettings));
+ const a = document.createElement("a");
+ a.setAttribute("href", data);
+ a.setAttribute("download", "iGateConfigurationBackup.json");
+ a.click();
+}
+
+document.getElementById("backup").onclick = backupSettings;
+
+document.getElementById("restore").onclick = function (e) {
+ e.preventDefault();
+
+ document.querySelector("input[type=file]").click();
+};
+
+document.querySelector("input[type=file]").onchange = function () {
+ const files = document.querySelector("input[type=file]").files;
+
+ if (!files.length) return;
+
+ const file = files.item(0);
+
+ const reader = new FileReader();
+ reader.readAsText(file);
+ reader.onload = () => {
+ const data = JSON.parse(reader.result);
+
+ loadSettings(data);
+ };
+};
+
+function fetchSettings() {
+ fetch("/configuration.json")
+ .then((response) => response.json())
+ .then((settings) => {
+ loadSettings(settings);
+ })
+ .catch((err) => {
+ console.error(err);
+
+ alert(`Failed to load configuration`);
+ });
+}
+
+const alwaysOnCheckbox = document.querySelector(
+ 'input[name="display.alwaysOn"]'
+);
+const timeoutInput = document.querySelector('input[name="display.timeout"]');
+
+alwaysOnCheckbox.addEventListener("change", function () {
+ timeoutInput.disabled = this.checked;
+});
+
+// timeoutInput.addEventListener("change", function () {
+// alwaysOnCheckbox.disabled = this.value !== "";
+// });
+
+const logCheckbox = document.querySelector('input[name="syslog.active"]');
+const serverField = document.querySelector('input[name="syslog.server"]');
+const portField = document.querySelector('input[name="syslog.port"]');
+
+logCheckbox.addEventListener("change", function () {
+ serverField.disabled = !this.checked;
+ portField.disabled = !this.checked;
+});
+
+function loadSettings(settings) {
+ currentSettings = settings;
+ // General
+ document.getElementById("callsign").value = settings.callsign;
+ document.getElementById("stationMode").value = settings.stationMode;
+ document.getElementById("bme.active").checked = settings.bme.active;
+ document.getElementById("iGateComment").value = settings.iGateComment;
+
+ document.querySelector(".list-networks").innerHTML = "";
+
+ // Networks
+ const wifiNetworks = settings.wifi.AP;
+ const networksContainer = document.querySelector(".list-networks");
+
+ let networkCount = 0;
+
+ wifiNetworks.forEach((network) => {
+ const networkElement = document.createElement("div");
+ networkElement.classList.add("row", "network", "border-bottom", "py-2");
+
+ // Increment the name, id, and for attributes
+ const attributeName = `wifi.AP.${networkCount}`;
+ networkElement.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ networksContainer.appendChild(networkElement);
+ networkCount++;
+ });
+
+ // APRS-IS
+ document.getElementById("aprs_is.server").value = settings.aprs_is.server;
+ document.getElementById("aprs_is.port").value = settings.aprs_is.port;
+ document.getElementById("aprs_is.reportingDistance").value =
+ settings.aprs_is.reportingDistance;
+ document.getElementById("aprs_is.passcode").value =
+ settings.aprs_is.passcode;
+
+ // Display
+ document.getElementById("display.alwaysOn").checked =
+ settings.display.alwaysOn;
+ document.getElementById("display.turn180").checked =
+ settings.display.turn180;
+ document.getElementById("display.timeout").value = settings.display.timeout;
+
+ if (settings.display.alwaysOn) {
+ timeoutInput.disabled = true;
+ }
+
+ // WiFi Auto AP
+ document.getElementById("wifi.autoAP.password").value =
+ settings.wifi.autoAP.password;
+ document.getElementById("wifi.autoAP.powerOff").value =
+ settings.wifi.autoAP.powerOff;
+
+ // Digi
+ document.getElementById("digi.comment").value = settings.digi.comment;
+ document.getElementById("digi.latitude").value = settings.digi.latitude;
+ document.getElementById("digi.longitude").value = settings.digi.longitude;
+
+ // OTA
+ document.getElementById("ota.username").value = settings.ota.username;
+ document.getElementById("ota.password").value = settings.ota.password;
+
+ // Beacon
+ document.getElementById("other.beaconInterval").value =
+ settings.other.beaconInterval;
+ document.getElementById("other.rememberStationTime").value =
+ settings.other.rememberStationTime;
+ document.getElementById("other.sendBatteryVoltage").checked =
+ settings.other.sendBatteryVoltage;
+ document.getElementById("other.externalVoltageMeasurement").checked =
+ settings.other.externalVoltageMeasurement;
+ document.getElementById("other.externalVoltagePin").value =
+ settings.other.externalVoltagePin;
+ document.getElementById("other.igateSendsLoRaBeacon").value =
+ settings.other.igateSendsLoRaBeacon;
+ document.getElementById("other.igateRepeatLoRaPackets").value =
+ settings.other.igateRepeatLoRaPackets;
+
+ // Syslog
+ document.getElementById("syslog.active").checked = settings.syslog.active;
+ document.getElementById("syslog.server").value = settings.syslog.server;
+ document.getElementById("syslog.port").value = settings.syslog.port;
+
+ if (settings.syslog.active) {
+ serverField.disabled = false;
+ portField.disabled = false;
+ }
+
+ // LoRa
+ document.getElementById("lora.digirepeaterTxFreq").value =
+ settings.lora.digirepeaterTxFreq;
+ document.getElementById("lora.iGateFreq").value = settings.lora.iGateFreq;
+ document.getElementById("lora.digirepeaterRxFreq").value =
+ settings.lora.digirepeaterRxFreq;
+ document.getElementById("lora.spreadingFactor").value =
+ settings.lora.spreadingFactor;
+ document.getElementById("lora.signalBandwidth").value =
+ settings.lora.signalBandwidth;
+ document.getElementById("lora.codingRate4").value =
+ settings.lora.codingRate4;
+ document.getElementById("lora.power").value = settings.lora.power;
+
+ updateImage();
+}
+
+const bmeCheckbox = document.querySelector("input[name='bme.active']");
+
+const stationModeSelect = document.querySelector("select[name='stationMode']");
+
+const digirepeaterTxFreqInput = document.querySelector(
+ "input[name='lora.digirepeaterRxFreq']"
+);
+
+function updateImage() {
+ const image = document.querySelector("img");
+
+ const isChecked = bmeCheckbox.checked;
+
+ if (isChecked) {
+ image.src =
+ "data:image/png;base64,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";
+ } else {
+ const selectedValue = stationModeSelect.value;
+
+ if (selectedValue === "1") {
+ if (bmeCheckbox.checked) {
+ image.src =
+ "data:image/png;base64,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";
+ } else {
+ image.src =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ6SURBVHgB7d0/axRBGMfx50IMWPoKkpA02gUsLfZdqOCLOPBFWAliHbCUVNpaxQNrIY0WVjZaJlUsIsZ5uDu9bGb/zMwzs/PM/L7wIG4Ocflwl7ndvVsihBBCmTajunpk5uHG33+aOSGkvhdmLs1cW+bKzDEhle2a+UZ22Pacrx6PlMRYXc/arrkkIKvIBxfISgrBBXLmSeACOdMkcYGcWTFwgZxJMXGBPHEpcIE8USlxgSzcbGD2KD0ukIXKGRfIAuWOC+TANOACOSAtuEAemQ10a2P2KV9cIA+0ZZltMzurOaT8cYHcUx+wJlwgd9QFrBEXyJZswAekFxfIrdq4GhZUQO7Itlq+QzoXVEC21AdcIm51yF3AJeNWhWwDLmFBBeRVmg4/Atmj2nGLR/bCbZrm2qWx/+7Ekwx5i9LGO/XFzF2qO97/r5QAOSUwcG+WBDkVMHDtRUdOAcz/ed4J4NqLihwbGLjjioYcExi4bkVBjgUMXL/EkWMAAzcsUWRpYODKJIYsCQxc2USQpYCBG6dgZAlg4MYtCDkUGLhp8kYOAQZu2ryQfYGT4S7PAKJVzsg+wEmfubNZbd+XOpgTsiswXpbzaDSyCzBw82oU8lhg4ObZIPIYYODmXS/yEDBwdcQ+72w/6AMGrq6OaHnLghv1AQNXX6/bG7qAPxNwNbbf3mAD5qf5EaEisgG/IVRMNuA9QsVkA94mpLWP7Q2pP5uE4vayvQHA5bQw86m90Qb8i5C22Kyx/cAG/IGQphj3ftcPbcBzM78JaWiN+73rATZgfvAzWn4SHeXbIC7Xtcg6MfOUgJxro3C5vlU0kPNsNC439DYJyHnlhMuNeR8M5DxyxuXGHugA8rR54XIuR7KAPE3euJzroUogpy0Il/M5Fg3kNAXjcr4nG4AcNxFcLuTc78nqz7e0/A7KLDo9PbV+nkn6Q2xnZ2c0n88pQmK4XOjJ/eyQm6YhxYnichLng/FyLZM4Lid1wh/IYUXB5SSv6ACyX9FwOelLdoDsVlRcLsY1WUAeV3RcLtZFd0Duj3EfUGRcLuZVlUC2lwyXi32Re/D75IuLC1osFtYDFXxAo73dts21MV/8wgc6PEqKm7LHZv5QnndASTV8p5U9unl7oaJ6QvUi23CLA+YdqhGZcQ/Jfmu/olrvVE3Ia9wdqgB4sxp+JzPuPv2/+XV1lYzcxq32Q34lvlzbcKsF5h0vCZlxD+g2btXApSCvF1TbBOB/ba4oNSOvn7lr3KpWzC5pXHgVf4RKOk3P5CqOUEmn5eW6CxfAA2n4ndyHC+AR5YwMXOFyWngx7i4h8XJABm7kpkQGbqKmQAZu4lIiA3eiUiADd+JiIgM3k2IgAzezJJGBm2kSyMDNvBBk4CrJBxm4ynJB/kHAVRnf3OucumGvzBxTBZV+eouhn5u5t7HtvZlXhBBCCCEUsb+qGDWF0d8l5AAAAABJRU5ErkJggg==";
+ }
+ digirepeaterTxFreqInput.disabled = true;
+ } else if (selectedValue === "2") {
+ if (bmeCheckbox.checked) {
+ image.src =
+ "data:image/png;base64,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";
+ } else {
+ image.src =
+ "data:image/png;base64,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";
+ }
+ digirepeaterTxFreqInput.disabled = true;
+ } else if (selectedValue === "3" || selectedValue === "4") {
+ image.src =
+ "data:image/png;base64,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";
+ digirepeaterTxFreqInput.disabled = selectedValue === "3";
+ } else if (selectedValue === "5") {
+ if (bmeCheckbox.checked) {
+ image.src =
+ "data:image/png;base64,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";
+ } else {
+ image.src =
+ "data:image/png;base64,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";
+ }
+ digirepeaterTxFreqInput.disabled = true;
+ }
+ }
+}
+
+stationModeSelect.addEventListener("change", updateImage);
+bmeCheckbox.addEventListener("change", updateImage);
+
+function toggleFields() {
+ const sendBatteryVoltageCheckbox = document.querySelector(
+ 'input[name="other.sendBatteryVoltage"]'
+ );
+ const externalVoltageMeasurementCheckbox = document.querySelector(
+ 'input[name="other.externalVoltageMeasurement"]'
+ );
+ const externalVoltagePinInput = document.querySelector(
+ 'input[name="other.externalVoltagePin"]'
+ );
+
+ externalVoltageMeasurementCheckbox.disabled =
+ !sendBatteryVoltageCheckbox.checked;
+ externalVoltagePinInput.disabled =
+ !externalVoltageMeasurementCheckbox.checked;
+}
+
+const sendBatteryVoltageCheckbox = document.querySelector(
+ 'input[name="other.sendBatteryVoltage"]'
+);
+const externalVoltageMeasurementCheckbox = document.querySelector(
+ 'input[name="other.externalVoltageMeasurement"]'
+);
+const externalVoltagePinInput = document.querySelector(
+ 'input[name="other.externalVoltagePin"]'
+);
+
+sendBatteryVoltageCheckbox.addEventListener("change", function () {
+ externalVoltageMeasurementCheckbox.disabled = !this.checked;
+ externalVoltagePinInput.disabled =
+ !externalVoltageMeasurementCheckbox.checked;
+});
+
+document.querySelector(".new button").addEventListener("click", function () {
+ const networksContainer = document.querySelector(".list-networks");
+
+ let networkCount = document.querySelectorAll(".network").length;
+
+ const networkElement = document.createElement("div");
+
+ networkElement.classList.add("row", "network", "border-bottom", "py-2");
+
+ // Increment the name, id, and for attributes
+ const attributeName = `wifi.AP.${networkCount}`;
+ networkElement.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ networksContainer.appendChild(networkElement);
+
+ networkCount++;
+
+ // Add the new network element to the end of the document
+ document.querySelector(".new").before(networkElement);
+});
+
+toggleFields();
+
+const form = document.querySelector("form");
+
+const saveModal = new bootstrap.Modal(document.getElementById("saveModal"), {
+ backdrop: "static",
+ keyboard: false,
+});
+
+const savedModal = new bootstrap.Modal(
+ document.getElementById("savedModal"),
+ {}
+);
+
+function checkConnection() {
+ const controller = new AbortController();
+
+ setTimeout(() => controller.abort(), 2000);
+
+ fetch("/status?_t=" + Date.now(), { signal: controller.signal })
+ .then(() => {
+ saveModal.hide();
+
+ savedModal.show();
+
+ setTimeout(function () {
+ savedModal.hide();
+ }, 3000);
+
+ fetchSettings();
+ })
+ .catch((err) => {
+ setTimeout(checkConnection, 0);
+ });
+}
+
+form.addEventListener("submit", async (event) => {
+ event.preventDefault();
+
+ document.getElementById("wifi.APs").value =
+ document.querySelectorAll(".network").length;
+
+ fetch(form.action, {
+ method: form.method,
+ body: new FormData(form),
+ });
+
+ saveModal.show();
+
+ setTimeout(checkConnection, 0);
+});
+
+fetchSettings();
diff --git a/data_embed/style.css b/data_embed/style.css
new file mode 100644
index 0000000..e69de29
diff --git a/installer/bin/esptool/esptool.py b/installer/bin/esptool/esptool.py
new file mode 100644
index 0000000..60e5bd3
--- /dev/null
+++ b/installer/bin/esptool/esptool.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+#
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# This executable script is a thin wrapper around the main functionality
+# in the esptool Python package
+
+# When updating this script, please also update espefuse.py and espsecure.py
+
+import contextlib
+import os
+import sys
+
+if os.name != "nt":
+ # Linux/macOS: remove current script directory to avoid importing this file
+ # as a module; we want to import the installed esptool module instead
+ with contextlib.suppress(ValueError):
+ if sys.path[0].endswith("/bin"):
+ sys.path.pop(0)
+ sys.path.remove(os.path.dirname(sys.executable))
+
+ # Linux/macOS: delete imported module entry to force Python to load
+ # the module from scratch; this enables importing esptool module in
+ # other Python scripts
+ with contextlib.suppress(KeyError):
+ del sys.modules["esptool"]
+
+import esptool
+
+if __name__ == "__main__":
+ esptool._main()
diff --git a/installer/bin/esptool/esptool/__init__.py b/installer/bin/esptool/esptool/__init__.py
new file mode 100644
index 0000000..24272f2
--- /dev/null
+++ b/installer/bin/esptool/esptool/__init__.py
@@ -0,0 +1,1055 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+__all__ = [
+ "chip_id",
+ "detect_chip",
+ "dump_mem",
+ "elf2image",
+ "erase_flash",
+ "erase_region",
+ "flash_id",
+ "get_security_info",
+ "image_info",
+ "load_ram",
+ "make_image",
+ "merge_bin",
+ "read_flash",
+ "read_flash_status",
+ "read_mac",
+ "read_mem",
+ "run",
+ "verify_flash",
+ "version",
+ "write_flash",
+ "write_flash_status",
+ "write_mem",
+]
+
+__version__ = "4.5.1"
+
+import argparse
+import inspect
+import os
+import shlex
+import sys
+import time
+import traceback
+
+from esptool.cmds import (
+ chip_id,
+ detect_chip,
+ detect_flash_size,
+ dump_mem,
+ elf2image,
+ erase_flash,
+ erase_region,
+ flash_id,
+ get_security_info,
+ image_info,
+ load_ram,
+ make_image,
+ merge_bin,
+ read_flash,
+ read_flash_status,
+ read_mac,
+ read_mem,
+ run,
+ verify_flash,
+ version,
+ write_flash,
+ write_flash_status,
+ write_mem,
+)
+from esptool.config import load_config_file
+from esptool.loader import DEFAULT_CONNECT_ATTEMPTS, ESPLoader, list_ports
+from esptool.targets import CHIP_DEFS, CHIP_LIST, ESP32ROM
+from esptool.util import (
+ FatalError,
+ NotImplementedInROMError,
+ flash_size_bytes,
+ strip_chip_name,
+)
+
+import serial
+
+
+def main(argv=None, esp=None):
+ """
+ Main function for esptool
+
+ argv - Optional override for default arguments parsing (that uses sys.argv),
+ can be a list of custom arguments as strings. Arguments and their values
+ need to be added as individual items to the list
+ e.g. "-b 115200" thus becomes ['-b', '115200'].
+
+ esp - Optional override of the connected device previously
+ returned by get_default_connected_device()
+ """
+
+ external_esp = esp is not None
+
+ parser = argparse.ArgumentParser(
+ description="esptool.py v%s - Espressif chips ROM Bootloader Utility"
+ % __version__,
+ prog="esptool",
+ )
+
+ parser.add_argument(
+ "--chip",
+ "-c",
+ help="Target chip type",
+ type=strip_chip_name,
+ choices=["auto"] + CHIP_LIST,
+ default=os.environ.get("ESPTOOL_CHIP", "auto"),
+ )
+
+ parser.add_argument(
+ "--port",
+ "-p",
+ help="Serial port device",
+ default=os.environ.get("ESPTOOL_PORT", None),
+ )
+
+ parser.add_argument(
+ "--baud",
+ "-b",
+ help="Serial port baud rate used when flashing/reading",
+ type=arg_auto_int,
+ default=os.environ.get("ESPTOOL_BAUD", ESPLoader.ESP_ROM_BAUD),
+ )
+
+ parser.add_argument(
+ "--before",
+ help="What to do before connecting to the chip",
+ choices=["default_reset", "usb_reset", "no_reset", "no_reset_no_sync"],
+ default=os.environ.get("ESPTOOL_BEFORE", "default_reset"),
+ )
+
+ parser.add_argument(
+ "--after",
+ "-a",
+ help="What to do after esptool.py is finished",
+ choices=["hard_reset", "soft_reset", "no_reset", "no_reset_stub"],
+ default=os.environ.get("ESPTOOL_AFTER", "hard_reset"),
+ )
+
+ parser.add_argument(
+ "--no-stub",
+ help="Disable launching the flasher stub, only talk to ROM bootloader. "
+ "Some features will not be available.",
+ action="store_true",
+ )
+
+ parser.add_argument(
+ "--trace",
+ "-t",
+ help="Enable trace-level output of esptool.py interactions.",
+ action="store_true",
+ )
+
+ parser.add_argument(
+ "--override-vddsdio",
+ help="Override ESP32 VDDSDIO internal voltage regulator (use with care)",
+ choices=ESP32ROM.OVERRIDE_VDDSDIO_CHOICES,
+ nargs="?",
+ )
+
+ parser.add_argument(
+ "--connect-attempts",
+ help=(
+ "Number of attempts to connect, negative or 0 for infinite. "
+ "Default: %d." % DEFAULT_CONNECT_ATTEMPTS
+ ),
+ type=int,
+ default=os.environ.get("ESPTOOL_CONNECT_ATTEMPTS", DEFAULT_CONNECT_ATTEMPTS),
+ )
+
+ subparsers = parser.add_subparsers(
+ dest="operation", help="Run esptool.py {command} -h for additional help"
+ )
+
+ def add_spi_connection_arg(parent):
+ parent.add_argument(
+ "--spi-connection",
+ "-sc",
+ help="ESP32-only argument. Override default SPI Flash connection. "
+ "Value can be SPI, HSPI or a comma-separated list of 5 I/O numbers "
+ "to use for SPI flash (CLK,Q,D,HD,CS).",
+ action=SpiConnectionAction,
+ )
+
+ parser_load_ram = subparsers.add_parser(
+ "load_ram", help="Download an image to RAM and execute"
+ )
+ parser_load_ram.add_argument("filename", help="Firmware image")
+
+ parser_dump_mem = subparsers.add_parser(
+ "dump_mem", help="Dump arbitrary memory to disk"
+ )
+ parser_dump_mem.add_argument("address", help="Base address", type=arg_auto_int)
+ parser_dump_mem.add_argument(
+ "size", help="Size of region to dump", type=arg_auto_int
+ )
+ parser_dump_mem.add_argument("filename", help="Name of binary dump")
+
+ parser_read_mem = subparsers.add_parser(
+ "read_mem", help="Read arbitrary memory location"
+ )
+ parser_read_mem.add_argument("address", help="Address to read", type=arg_auto_int)
+
+ parser_write_mem = subparsers.add_parser(
+ "write_mem", help="Read-modify-write to arbitrary memory location"
+ )
+ parser_write_mem.add_argument("address", help="Address to write", type=arg_auto_int)
+ parser_write_mem.add_argument("value", help="Value", type=arg_auto_int)
+ parser_write_mem.add_argument(
+ "mask",
+ help="Mask of bits to write",
+ type=arg_auto_int,
+ nargs="?",
+ default="0xFFFFFFFF",
+ )
+
+ def add_spi_flash_subparsers(parent, allow_keep, auto_detect):
+ """Add common parser arguments for SPI flash properties"""
+ extra_keep_args = ["keep"] if allow_keep else []
+
+ if auto_detect and allow_keep:
+ extra_fs_message = ", detect, or keep"
+ flash_sizes = ["detect", "keep"]
+ elif auto_detect:
+ extra_fs_message = ", or detect"
+ flash_sizes = ["detect"]
+ elif allow_keep:
+ extra_fs_message = ", or keep"
+ flash_sizes = ["keep"]
+ else:
+ extra_fs_message = ""
+ flash_sizes = []
+
+ parent.add_argument(
+ "--flash_freq",
+ "-ff",
+ help="SPI Flash frequency",
+ choices=extra_keep_args
+ + [
+ "80m",
+ "60m",
+ "48m",
+ "40m",
+ "30m",
+ "26m",
+ "24m",
+ "20m",
+ "16m",
+ "15m",
+ "12m",
+ ],
+ default=os.environ.get("ESPTOOL_FF", "keep" if allow_keep else None),
+ )
+ parent.add_argument(
+ "--flash_mode",
+ "-fm",
+ help="SPI Flash mode",
+ choices=extra_keep_args + ["qio", "qout", "dio", "dout"],
+ default=os.environ.get("ESPTOOL_FM", "keep" if allow_keep else "qio"),
+ )
+ parent.add_argument(
+ "--flash_size",
+ "-fs",
+ help="SPI Flash size in MegaBytes "
+ "(1MB, 2MB, 4MB, 8MB, 16MB, 32MB, 64MB, 128MB) "
+ "plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)" + extra_fs_message,
+ choices=flash_sizes
+ + [
+ "256KB",
+ "512KB",
+ "1MB",
+ "2MB",
+ "2MB-c1",
+ "4MB",
+ "4MB-c1",
+ "8MB",
+ "16MB",
+ "32MB",
+ "64MB",
+ "128MB",
+ ],
+ default=os.environ.get("ESPTOOL_FS", "keep" if allow_keep else "1MB"),
+ )
+ add_spi_connection_arg(parent)
+
+ parser_write_flash = subparsers.add_parser(
+ "write_flash", help="Write a binary blob to flash"
+ )
+
+ parser_write_flash.add_argument(
+ "addr_filename",
+ metavar=" ",
+ help="Address followed by binary filename, separated by space",
+ action=AddrFilenamePairAction,
+ )
+ parser_write_flash.add_argument(
+ "--erase-all",
+ "-e",
+ help="Erase all regions of flash (not just write areas) before programming",
+ action="store_true",
+ )
+
+ add_spi_flash_subparsers(parser_write_flash, allow_keep=True, auto_detect=True)
+ parser_write_flash.add_argument(
+ "--no-progress", "-p", help="Suppress progress output", action="store_true"
+ )
+ parser_write_flash.add_argument(
+ "--verify",
+ help="Verify just-written data on flash "
+ "(mostly superfluous, data is read back during flashing)",
+ action="store_true",
+ )
+ parser_write_flash.add_argument(
+ "--encrypt",
+ help="Apply flash encryption when writing data "
+ "(required correct efuse settings)",
+ action="store_true",
+ )
+ # In order to not break backward compatibility,
+ # our list of encrypted files to flash is a new parameter
+ parser_write_flash.add_argument(
+ "--encrypt-files",
+ metavar=" ",
+ help="Files to be encrypted on the flash. "
+ "Address followed by binary filename, separated by space.",
+ action=AddrFilenamePairAction,
+ )
+ parser_write_flash.add_argument(
+ "--ignore-flash-encryption-efuse-setting",
+ help="Ignore flash encryption efuse settings ",
+ action="store_true",
+ )
+ parser_write_flash.add_argument(
+ "--force",
+ help="Force write, skip security and compatibility checks. Use with caution!",
+ action="store_true",
+ )
+
+ compress_args = parser_write_flash.add_mutually_exclusive_group(required=False)
+ compress_args.add_argument(
+ "--compress",
+ "-z",
+ help="Compress data in transfer (default unless --no-stub is specified)",
+ action="store_true",
+ default=None,
+ )
+ compress_args.add_argument(
+ "--no-compress",
+ "-u",
+ help="Disable data compression during transfer "
+ "(default if --no-stub is specified)",
+ action="store_true",
+ )
+
+ subparsers.add_parser("run", help="Run application code in flash")
+
+ parser_image_info = subparsers.add_parser(
+ "image_info", help="Dump headers from an application image"
+ )
+ parser_image_info.add_argument("filename", help="Image file to parse")
+ parser_image_info.add_argument(
+ "--version",
+ "-v",
+ help="Output format version (1 - legacy, 2 - extended)",
+ choices=["1", "2"],
+ default="1",
+ )
+
+ parser_make_image = subparsers.add_parser(
+ "make_image", help="Create an application image from binary files"
+ )
+ parser_make_image.add_argument("output", help="Output image file")
+ parser_make_image.add_argument(
+ "--segfile", "-f", action="append", help="Segment input file"
+ )
+ parser_make_image.add_argument(
+ "--segaddr",
+ "-a",
+ action="append",
+ help="Segment base address",
+ type=arg_auto_int,
+ )
+ parser_make_image.add_argument(
+ "--entrypoint",
+ "-e",
+ help="Address of entry point",
+ type=arg_auto_int,
+ default=0,
+ )
+
+ parser_elf2image = subparsers.add_parser(
+ "elf2image", help="Create an application image from ELF file"
+ )
+ parser_elf2image.add_argument("input", help="Input ELF file")
+ parser_elf2image.add_argument(
+ "--output",
+ "-o",
+ help="Output filename prefix (for version 1 image), "
+ "or filename (for version 2 single image)",
+ type=str,
+ )
+ parser_elf2image.add_argument(
+ "--version",
+ "-e",
+ help="Output image version",
+ choices=["1", "2", "3"],
+ default="1",
+ )
+ parser_elf2image.add_argument(
+ # it kept for compatibility
+ # Minimum chip revision (deprecated, consider using --min-rev-full)
+ "--min-rev",
+ "-r",
+ help=argparse.SUPPRESS,
+ type=int,
+ choices=range(256),
+ metavar="{0, ... 255}",
+ default=0,
+ )
+ parser_elf2image.add_argument(
+ "--min-rev-full",
+ help="Minimal chip revision (in format: major * 100 + minor)",
+ type=int,
+ choices=range(65536),
+ metavar="{0, ... 65535}",
+ default=0,
+ )
+ parser_elf2image.add_argument(
+ "--max-rev-full",
+ help="Maximal chip revision (in format: major * 100 + minor)",
+ type=int,
+ choices=range(65536),
+ metavar="{0, ... 65535}",
+ default=65535,
+ )
+ parser_elf2image.add_argument(
+ "--secure-pad",
+ action="store_true",
+ help="Pad image so once signed it will end on a 64KB boundary. "
+ "For Secure Boot v1 images only.",
+ )
+ parser_elf2image.add_argument(
+ "--secure-pad-v2",
+ action="store_true",
+ help="Pad image to 64KB, so once signed its signature sector will"
+ "start at the next 64K block. For Secure Boot v2 images only.",
+ )
+ parser_elf2image.add_argument(
+ "--elf-sha256-offset",
+ help="If set, insert SHA256 hash (32 bytes) of the input ELF file "
+ "at specified offset in the binary.",
+ type=arg_auto_int,
+ default=None,
+ )
+ parser_elf2image.add_argument(
+ "--dont-append-digest",
+ dest="append_digest",
+ help="Don't append a SHA256 digest of the entire image after the checksum. "
+ "This argument is not supported and ignored for ESP8266.",
+ action="store_false",
+ default=True,
+ )
+ parser_elf2image.add_argument(
+ "--use_segments",
+ help="If set, ELF segments will be used instead of ELF sections "
+ "to genereate the image.",
+ action="store_true",
+ )
+ parser_elf2image.add_argument(
+ "--flash-mmu-page-size",
+ help="Change flash MMU page size.",
+ choices=["64KB", "32KB", "16KB", "8KB"],
+ )
+ parser_elf2image.add_argument(
+ "--pad-to-size",
+ help="The block size with which the final binary image after padding "
+ "must be aligned to. Value 0xFF is used for padding, similar to erase_flash",
+ default=None,
+ )
+
+ add_spi_flash_subparsers(parser_elf2image, allow_keep=False, auto_detect=False)
+
+ subparsers.add_parser("read_mac", help="Read MAC address from OTP ROM")
+
+ subparsers.add_parser("chip_id", help="Read Chip ID from OTP ROM")
+
+ parser_flash_id = subparsers.add_parser(
+ "flash_id", help="Read SPI flash manufacturer and device ID"
+ )
+ add_spi_connection_arg(parser_flash_id)
+
+ parser_read_status = subparsers.add_parser(
+ "read_flash_status", help="Read SPI flash status register"
+ )
+
+ add_spi_connection_arg(parser_read_status)
+ parser_read_status.add_argument(
+ "--bytes",
+ help="Number of bytes to read (1-3)",
+ type=int,
+ choices=[1, 2, 3],
+ default=2,
+ )
+
+ parser_write_status = subparsers.add_parser(
+ "write_flash_status", help="Write SPI flash status register"
+ )
+
+ add_spi_connection_arg(parser_write_status)
+ parser_write_status.add_argument(
+ "--non-volatile",
+ help="Write non-volatile bits (use with caution)",
+ action="store_true",
+ )
+ parser_write_status.add_argument(
+ "--bytes",
+ help="Number of status bytes to write (1-3)",
+ type=int,
+ choices=[1, 2, 3],
+ default=2,
+ )
+ parser_write_status.add_argument("value", help="New value", type=arg_auto_int)
+
+ parser_read_flash = subparsers.add_parser(
+ "read_flash", help="Read SPI flash content"
+ )
+ add_spi_connection_arg(parser_read_flash)
+ parser_read_flash.add_argument("address", help="Start address", type=arg_auto_int)
+ parser_read_flash.add_argument(
+ "size", help="Size of region to dump", type=arg_auto_int
+ )
+ parser_read_flash.add_argument("filename", help="Name of binary dump")
+ parser_read_flash.add_argument(
+ "--no-progress", "-p", help="Suppress progress output", action="store_true"
+ )
+
+ parser_verify_flash = subparsers.add_parser(
+ "verify_flash", help="Verify a binary blob against flash"
+ )
+ parser_verify_flash.add_argument(
+ "addr_filename",
+ help="Address and binary file to verify there, separated by space",
+ action=AddrFilenamePairAction,
+ )
+ parser_verify_flash.add_argument(
+ "--diff", "-d", help="Show differences", choices=["no", "yes"], default="no"
+ )
+ add_spi_flash_subparsers(parser_verify_flash, allow_keep=True, auto_detect=True)
+
+ parser_erase_flash = subparsers.add_parser(
+ "erase_flash", help="Perform Chip Erase on SPI flash"
+ )
+ parser_erase_flash.add_argument(
+ "--force",
+ help="Erase flash even if security features are enabled. Use with caution!",
+ action="store_true",
+ )
+ add_spi_connection_arg(parser_erase_flash)
+
+ parser_erase_region = subparsers.add_parser(
+ "erase_region", help="Erase a region of the flash"
+ )
+ parser_erase_region.add_argument(
+ "--force",
+ help="Erase region even if security features are enabled. Use with caution!",
+ action="store_true",
+ )
+ add_spi_connection_arg(parser_erase_region)
+ parser_erase_region.add_argument(
+ "address", help="Start address (must be multiple of 4096)", type=arg_auto_int
+ )
+ parser_erase_region.add_argument(
+ "size",
+ help="Size of region to erase (must be multiple of 4096)",
+ type=arg_auto_int,
+ )
+
+ parser_merge_bin = subparsers.add_parser(
+ "merge_bin",
+ help="Merge multiple raw binary files into a single file for later flashing",
+ )
+
+ parser_merge_bin.add_argument(
+ "--output", "-o", help="Output filename", type=str, required=True
+ )
+ parser_merge_bin.add_argument(
+ "--format", "-f", help="Format of the output file", choices="raw", default="raw"
+ ) # for future expansion
+ add_spi_flash_subparsers(parser_merge_bin, allow_keep=True, auto_detect=False)
+
+ parser_merge_bin.add_argument(
+ "--target-offset",
+ "-t",
+ help="Target offset where the output file will be flashed",
+ type=arg_auto_int,
+ default=0,
+ )
+ parser_merge_bin.add_argument(
+ "--fill-flash-size",
+ help="If set, the final binary file will be padded with FF "
+ "bytes up to this flash size.",
+ choices=[
+ "256KB",
+ "512KB",
+ "1MB",
+ "2MB",
+ "4MB",
+ "8MB",
+ "16MB",
+ "32MB",
+ "64MB",
+ "128MB",
+ ],
+ )
+ parser_merge_bin.add_argument(
+ "addr_filename",
+ metavar=" ",
+ help="Address followed by binary filename, separated by space",
+ action=AddrFilenamePairAction,
+ )
+
+ subparsers.add_parser("get_security_info", help="Get some security-related data")
+
+ subparsers.add_parser("version", help="Print esptool version")
+
+ # internal sanity check - every operation matches a module function of the same name
+ for operation in subparsers.choices.keys():
+ assert operation in globals(), "%s should be a module function" % operation
+
+ argv = expand_file_arguments(argv or sys.argv[1:])
+
+ args = parser.parse_args(argv)
+ print("esptool.py v%s" % __version__)
+ load_config_file(verbose=True)
+
+ # operation function can take 1 arg (args), 2 args (esp, arg)
+ # or be a member function of the ESPLoader class.
+
+ if args.operation is None:
+ parser.print_help()
+ sys.exit(1)
+
+ # Forbid the usage of both --encrypt, which means encrypt all the given files,
+ # and --encrypt-files, which represents the list of files to encrypt.
+ # The reason is that allowing both at the same time increases the chances of
+ # having contradictory lists (e.g. one file not available in one of list).
+ if (
+ args.operation == "write_flash"
+ and args.encrypt
+ and args.encrypt_files is not None
+ ):
+ raise FatalError(
+ "Options --encrypt and --encrypt-files "
+ "must not be specified at the same time."
+ )
+
+ operation_func = globals()[args.operation]
+ operation_args = inspect.getfullargspec(operation_func).args
+
+ if (
+ operation_args[0] == "esp"
+ ): # operation function takes an ESPLoader connection object
+ if args.before != "no_reset_no_sync":
+ initial_baud = min(
+ ESPLoader.ESP_ROM_BAUD, args.baud
+ ) # don't sync faster than the default baud rate
+ else:
+ initial_baud = args.baud
+
+ if args.port is None:
+ ser_list = get_port_list()
+ print("Found %d serial ports" % len(ser_list))
+ else:
+ ser_list = [args.port]
+ esp = esp or get_default_connected_device(
+ ser_list,
+ port=args.port,
+ connect_attempts=args.connect_attempts,
+ initial_baud=initial_baud,
+ chip=args.chip,
+ trace=args.trace,
+ before=args.before,
+ )
+
+ if esp is None:
+ raise FatalError(
+ "Could not connect to an Espressif device "
+ "on any of the %d available serial ports." % len(ser_list)
+ )
+
+ if esp.secure_download_mode:
+ print("Chip is %s in Secure Download Mode" % esp.CHIP_NAME)
+ else:
+ print("Chip is %s" % (esp.get_chip_description()))
+ print("Features: %s" % ", ".join(esp.get_chip_features()))
+ print("Crystal is %dMHz" % esp.get_crystal_freq())
+ read_mac(esp, args)
+
+ if not args.no_stub:
+ if esp.secure_download_mode:
+ print(
+ "WARNING: Stub loader is not supported in Secure Download Mode, "
+ "setting --no-stub"
+ )
+ args.no_stub = True
+ elif not esp.IS_STUB and esp.stub_is_disabled:
+ print(
+ "WARNING: Stub loader has been disabled for compatibility, "
+ "setting --no-stub"
+ )
+ args.no_stub = True
+ else:
+ esp = esp.run_stub()
+
+ if args.override_vddsdio:
+ esp.override_vddsdio(args.override_vddsdio)
+
+ if args.baud > initial_baud:
+ try:
+ esp.change_baud(args.baud)
+ except NotImplementedInROMError:
+ print(
+ "WARNING: ROM doesn't support changing baud rate. "
+ "Keeping initial baud rate %d" % initial_baud
+ )
+
+ # override common SPI flash parameter stuff if configured to do so
+ if hasattr(args, "spi_connection") and args.spi_connection is not None:
+ if esp.CHIP_NAME != "ESP32":
+ raise FatalError(
+ "Chip %s does not support --spi-connection option." % esp.CHIP_NAME
+ )
+ print("Configuring SPI flash mode...")
+ esp.flash_spi_attach(args.spi_connection)
+ elif args.no_stub:
+ print("Enabling default SPI flash mode...")
+ # ROM loader doesn't enable flash unless we explicitly do it
+ esp.flash_spi_attach(0)
+
+ # XMC chip startup sequence
+ XMC_VENDOR_ID = 0x20
+
+ def is_xmc_chip_strict():
+ id = esp.flash_id()
+ rdid = ((id & 0xFF) << 16) | ((id >> 16) & 0xFF) | (id & 0xFF00)
+
+ vendor_id = (rdid >> 16) & 0xFF
+ mfid = (rdid >> 8) & 0xFF
+ cpid = rdid & 0xFF
+
+ if vendor_id != XMC_VENDOR_ID:
+ return False
+
+ matched = False
+ if mfid == 0x40:
+ if cpid >= 0x13 and cpid <= 0x20:
+ matched = True
+ elif mfid == 0x41:
+ if cpid >= 0x17 and cpid <= 0x20:
+ matched = True
+ elif mfid == 0x50:
+ if cpid >= 0x15 and cpid <= 0x16:
+ matched = True
+ return matched
+
+ def flash_xmc_startup():
+ # If the RDID value is a valid XMC one, may skip the flow
+ fast_check = True
+ if fast_check and is_xmc_chip_strict():
+ return # Successful XMC flash chip boot-up detected by RDID, skipping.
+
+ sfdp_mfid_addr = 0x10
+ mf_id = esp.read_spiflash_sfdp(sfdp_mfid_addr, 8)
+ if mf_id != XMC_VENDOR_ID: # Non-XMC chip detected by SFDP Read, skipping.
+ return
+
+ print(
+ "WARNING: XMC flash chip boot-up failure detected! "
+ "Running XMC25QHxxC startup flow"
+ )
+ esp.run_spiflash_command(0xB9) # Enter DPD
+ esp.run_spiflash_command(0x79) # Enter UDPD
+ esp.run_spiflash_command(0xFF) # Exit UDPD
+ time.sleep(0.002) # Delay tXUDPD
+ esp.run_spiflash_command(0xAB) # Release Power-Down
+ time.sleep(0.00002)
+ # Check for success
+ if not is_xmc_chip_strict():
+ print("WARNING: XMC flash boot-up fix failed.")
+ print("XMC flash chip boot-up fix successful!")
+
+ # Check flash chip connection
+ if not esp.secure_download_mode:
+ try:
+ flash_id = esp.flash_id()
+ if flash_id in (0xFFFFFF, 0x000000):
+ print(
+ "WARNING: Failed to communicate with the flash chip, "
+ "read/write operations will fail. "
+ "Try checking the chip connections or removing "
+ "any other hardware connected to IOs."
+ )
+ except FatalError as e:
+ raise FatalError(f"Unable to verify flash chip connection ({e}).")
+
+ # Check if XMC SPI flash chip booted-up successfully, fix if not
+ if not esp.secure_download_mode:
+ try:
+ flash_xmc_startup()
+ except FatalError as e:
+ esp.trace(f"Unable to perform XMC flash chip startup sequence ({e}).")
+
+ if hasattr(args, "flash_size"):
+ print("Configuring flash size...")
+ detect_flash_size(esp, args)
+ if args.flash_size != "keep": # TODO: should set this even with 'keep'
+ esp.flash_set_parameters(flash_size_bytes(args.flash_size))
+ # Check if stub supports chosen flash size
+ if esp.IS_STUB and args.flash_size in ("32MB", "64MB", "128MB"):
+ print(
+ "WARNING: Flasher stub doesn't fully support flash size larger "
+ "than 16MB, in case of failure use --no-stub."
+ )
+
+ if esp.IS_STUB and hasattr(args, "address") and hasattr(args, "size"):
+ if args.address + args.size > 0x1000000:
+ print(
+ "WARNING: Flasher stub doesn't fully support flash size larger "
+ "than 16MB, in case of failure use --no-stub."
+ )
+
+ try:
+ operation_func(esp, args)
+ finally:
+ try: # Clean up AddrFilenamePairAction files
+ for address, argfile in args.addr_filename:
+ argfile.close()
+ except AttributeError:
+ pass
+
+ # Handle post-operation behaviour (reset or other)
+ if operation_func == load_ram:
+ # the ESP is now running the loaded image, so let it run
+ print("Exiting immediately.")
+ elif args.after == "hard_reset":
+ esp.hard_reset()
+ elif args.after == "soft_reset":
+ print("Soft resetting...")
+ # flash_finish will trigger a soft reset
+ esp.soft_reset(False)
+ elif args.after == "no_reset_stub":
+ print("Staying in flasher stub.")
+ else: # args.after == 'no_reset'
+ print("Staying in bootloader.")
+ if esp.IS_STUB:
+ esp.soft_reset(True) # exit stub back to ROM loader
+
+ if not external_esp:
+ esp._port.close()
+
+ else:
+ operation_func(args)
+
+
+def arg_auto_int(x):
+ return int(x, 0)
+
+
+def get_port_list():
+ if list_ports is None:
+ raise FatalError(
+ "Listing all serial ports is currently not available. "
+ "Please try to specify the port when running esptool.py or update "
+ "the pyserial package to the latest version"
+ )
+ return sorted(ports.device for ports in list_ports.comports())
+
+
+def expand_file_arguments(argv):
+ """
+ Any argument starting with "@" gets replaced with all values read from a text file.
+ Text file arguments can be split by newline or by space.
+ Values are added "as-is", as if they were specified in this order
+ on the command line.
+ """
+ new_args = []
+ expanded = False
+ for arg in argv:
+ if arg.startswith("@"):
+ expanded = True
+ with open(arg[1:], "r") as f:
+ for line in f.readlines():
+ new_args += shlex.split(line)
+ else:
+ new_args.append(arg)
+ if expanded:
+ print("esptool %s" % (" ".join(new_args[1:])))
+ return new_args
+ return argv
+
+
+def get_default_connected_device(
+ serial_list,
+ port,
+ connect_attempts,
+ initial_baud,
+ chip="auto",
+ trace=False,
+ before="default_reset",
+):
+ _esp = None
+ for each_port in reversed(serial_list):
+ print("Serial port %s" % each_port)
+ try:
+ if chip == "auto":
+ _esp = detect_chip(
+ each_port, initial_baud, before, trace, connect_attempts
+ )
+ else:
+ chip_class = CHIP_DEFS[chip]
+ _esp = chip_class(each_port, initial_baud, trace)
+ _esp.connect(before, connect_attempts)
+ break
+ except (FatalError, OSError) as err:
+ if port is not None:
+ raise
+ print("%s failed to connect: %s" % (each_port, err))
+ if _esp and _esp._port:
+ _esp._port.close()
+ _esp = None
+ return _esp
+
+
+class SpiConnectionAction(argparse.Action):
+ """
+ Custom action to parse 'spi connection' override.
+ Values are SPI, HSPI, or a sequence of 5 pin numbers separated by commas.
+ """
+
+ def __call__(self, parser, namespace, value, option_string=None):
+ if value.upper() == "SPI":
+ value = 0
+ elif value.upper() == "HSPI":
+ value = 1
+ elif "," in value:
+ values = value.split(",")
+ if len(values) != 5:
+ raise argparse.ArgumentError(
+ self,
+ "%s is not a valid list of comma-separate pin numbers. "
+ "Must be 5 numbers - CLK,Q,D,HD,CS." % value,
+ )
+ try:
+ values = tuple(int(v, 0) for v in values)
+ except ValueError:
+ raise argparse.ArgumentError(
+ self,
+ "%s is not a valid argument. All pins must be numeric values"
+ % values,
+ )
+ if any([v for v in values if v > 33 or v < 0]):
+ raise argparse.ArgumentError(
+ self, "Pin numbers must be in the range 0-33."
+ )
+ # encode the pin numbers as a 32-bit integer with packed 6-bit values,
+ # the same way ESP32 ROM takes them
+ # TODO: make this less ESP32 ROM specific somehow...
+ clk, q, d, hd, cs = values
+ value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk
+ else:
+ raise argparse.ArgumentError(
+ self,
+ "%s is not a valid spi-connection value. "
+ "Values are SPI, HSPI, or a sequence of 5 pin numbers CLK,Q,D,HD,CS)."
+ % value,
+ )
+ setattr(namespace, self.dest, value)
+
+
+class AddrFilenamePairAction(argparse.Action):
+ """Custom parser class for the address/filename pairs passed as arguments"""
+
+ def __init__(self, option_strings, dest, nargs="+", **kwargs):
+ super(AddrFilenamePairAction, self).__init__(
+ option_strings, dest, nargs, **kwargs
+ )
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ # validate pair arguments
+ pairs = []
+ for i in range(0, len(values), 2):
+ try:
+ address = int(values[i], 0)
+ except ValueError:
+ raise argparse.ArgumentError(
+ self, 'Address "%s" must be a number' % values[i]
+ )
+ try:
+ argfile = open(values[i + 1], "rb")
+ except IOError as e:
+ raise argparse.ArgumentError(self, e)
+ except IndexError:
+ raise argparse.ArgumentError(
+ self,
+ "Must be pairs of an address "
+ "and the binary filename to write there",
+ )
+ pairs.append((address, argfile))
+
+ # Sort the addresses and check for overlapping
+ end = 0
+ for address, argfile in sorted(pairs, key=lambda x: x[0]):
+ argfile.seek(0, 2) # seek to end
+ size = argfile.tell()
+ argfile.seek(0)
+ sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)
+ sector_end = (
+ (address + size + ESPLoader.FLASH_SECTOR_SIZE - 1)
+ & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)
+ ) - 1
+ if sector_start < end:
+ message = "Detected overlap at address: 0x%x for file: %s" % (
+ address,
+ argfile.name,
+ )
+ raise argparse.ArgumentError(self, message)
+ end = sector_end
+ setattr(namespace, self.dest, pairs)
+
+
+def _main():
+ try:
+ main()
+ except FatalError as e:
+ print(f"\nA fatal error occurred: {e}")
+ sys.exit(2)
+ except serial.serialutil.SerialException as e:
+ print(f"\nA serial exception error occurred: {e}")
+ print(
+ "Note: This error originates from pySerial. "
+ "It is likely not a problem with esptool, "
+ "but with the hardware connection or drivers."
+ )
+ print(
+ "For troubleshooting steps visit: "
+ "https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html"
+ )
+ sys.exit(1)
+ except StopIteration:
+ print(traceback.format_exc())
+ print("A fatal error occurred: The chip stopped responding.")
+ sys.exit(2)
+
+
+if __name__ == "__main__":
+ _main()
diff --git a/installer/bin/esptool/esptool/__main__.py b/installer/bin/esptool/esptool/__main__.py
new file mode 100644
index 0000000..11e3bce
--- /dev/null
+++ b/installer/bin/esptool/esptool/__main__.py
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import esptool
+
+if __name__ == "__main__":
+ esptool._main()
diff --git a/installer/bin/esptool/esptool/bin_image.py b/installer/bin/esptool/esptool/bin_image.py
new file mode 100644
index 0000000..b001a1e
--- /dev/null
+++ b/installer/bin/esptool/esptool/bin_image.py
@@ -0,0 +1,1239 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import binascii
+import copy
+import hashlib
+import io
+import os
+import re
+import struct
+
+from .loader import ESPLoader
+from .targets import (
+ ESP32C2ROM,
+ ESP32C3ROM,
+ ESP32C6BETAROM,
+ ESP32C6ROM,
+ ESP32H2BETA1ROM,
+ ESP32H2BETA2ROM,
+ ESP32H2ROM,
+ ESP32ROM,
+ ESP32S2ROM,
+ ESP32S3BETA2ROM,
+ ESP32S3ROM,
+ ESP8266ROM,
+)
+from .util import FatalError, byte, pad_to
+
+
+def align_file_position(f, size):
+ """Align the position in the file to the next block of specified size"""
+ align = (size - 1) - (f.tell() % size)
+ f.seek(align, 1)
+
+
+def LoadFirmwareImage(chip, image_file):
+ """
+ Load a firmware image. Can be for any supported SoC.
+
+ ESP8266 images will be examined to determine if they are original ROM firmware
+ images (ESP8266ROMFirmwareImage) or "v2" OTA bootloader images.
+
+ Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1)
+ or ESP8266V2FirmwareImage (v2).
+ """
+
+ def select_image_class(f, chip):
+ chip = re.sub(r"[-()]", "", chip.lower())
+ if chip != "esp8266":
+ return {
+ "esp32": ESP32FirmwareImage,
+ "esp32s2": ESP32S2FirmwareImage,
+ "esp32s3beta2": ESP32S3BETA2FirmwareImage,
+ "esp32s3": ESP32S3FirmwareImage,
+ "esp32c3": ESP32C3FirmwareImage,
+ "esp32c6beta": ESP32C6BETAFirmwareImage,
+ "esp32h2beta1": ESP32H2BETA1FirmwareImage,
+ "esp32h2beta2": ESP32H2BETA2FirmwareImage,
+ "esp32c2": ESP32C2FirmwareImage,
+ "esp32c6": ESP32C6FirmwareImage,
+ "esp32h2": ESP32H2FirmwareImage,
+ }[chip](f)
+ else: # Otherwise, ESP8266 so look at magic to determine the image type
+ magic = ord(f.read(1))
+ f.seek(0)
+ if magic == ESPLoader.ESP_IMAGE_MAGIC:
+ return ESP8266ROMFirmwareImage(f)
+ elif magic == ESP8266V2FirmwareImage.IMAGE_V2_MAGIC:
+ return ESP8266V2FirmwareImage(f)
+ else:
+ raise FatalError("Invalid image magic number: %d" % magic)
+
+ if isinstance(image_file, str):
+ with open(image_file, "rb") as f:
+ return select_image_class(f, chip)
+ return select_image_class(image_file, chip)
+
+
+class ImageSegment(object):
+ """Wrapper class for a segment in an ESP image
+ (very similar to a section in an ELFImage also)"""
+
+ def __init__(self, addr, data, file_offs=None):
+ self.addr = addr
+ self.data = data
+ self.file_offs = file_offs
+ self.include_in_checksum = True
+ if self.addr != 0:
+ self.pad_to_alignment(
+ 4
+ ) # pad all "real" ImageSegments 4 byte aligned length
+
+ def copy_with_new_addr(self, new_addr):
+ """Return a new ImageSegment with same data, but mapped at
+ a new address."""
+ return ImageSegment(new_addr, self.data, 0)
+
+ def split_image(self, split_len):
+ """Return a new ImageSegment which splits "split_len" bytes
+ from the beginning of the data. Remaining bytes are kept in
+ this segment object (and the start address is adjusted to match.)"""
+ result = copy.copy(self)
+ result.data = self.data[:split_len]
+ self.data = self.data[split_len:]
+ self.addr += split_len
+ self.file_offs = None
+ result.file_offs = None
+ return result
+
+ def __repr__(self):
+ r = "len 0x%05x load 0x%08x" % (len(self.data), self.addr)
+ if self.file_offs is not None:
+ r += " file_offs 0x%08x" % (self.file_offs)
+ return r
+
+ def get_memory_type(self, image):
+ """
+ Return a list describing the memory type(s) that is covered by this
+ segment's start address.
+ """
+ return [
+ map_range[2]
+ for map_range in image.ROM_LOADER.MEMORY_MAP
+ if map_range[0] <= self.addr < map_range[1]
+ ]
+
+ def pad_to_alignment(self, alignment):
+ self.data = pad_to(self.data, alignment, b"\x00")
+
+
+class ELFSection(ImageSegment):
+ """Wrapper class for a section in an ELF image, has a section
+ name as well as the common properties of an ImageSegment."""
+
+ def __init__(self, name, addr, data):
+ super(ELFSection, self).__init__(addr, data)
+ self.name = name.decode("utf-8")
+
+ def __repr__(self):
+ return "%s %s" % (self.name, super(ELFSection, self).__repr__())
+
+
+class BaseFirmwareImage(object):
+ SEG_HEADER_LEN = 8
+ SHA256_DIGEST_LEN = 32
+
+ """ Base class with common firmware image functions """
+
+ def __init__(self):
+ self.segments = []
+ self.entrypoint = 0
+ self.elf_sha256 = None
+ self.elf_sha256_offset = 0
+ self.pad_to_size = 0
+
+ def load_common_header(self, load_file, expected_magic):
+ (
+ magic,
+ segments,
+ self.flash_mode,
+ self.flash_size_freq,
+ self.entrypoint,
+ ) = struct.unpack(" 16:
+ raise FatalError(
+ "Invalid segment count %d (max 16). "
+ "Usually this indicates a linker script problem." % len(self.segments)
+ )
+
+ def load_segment(self, f, is_irom_segment=False):
+ """Load the next segment from the image file"""
+ file_offs = f.tell()
+ (offset, size) = struct.unpack(" 0x40200000 or offset < 0x3FFE0000 or size > 65536:
+ print("WARNING: Suspicious segment 0x%x, length %d" % (offset, size))
+
+ def maybe_patch_segment_data(self, f, segment_data):
+ """
+ If SHA256 digest of the ELF file needs to be inserted into this segment, do so.
+ Returns segment data.
+ """
+ segment_len = len(segment_data)
+ file_pos = f.tell() # file_pos is position in the .bin file
+ if (
+ self.elf_sha256_offset >= file_pos
+ and self.elf_sha256_offset < file_pos + segment_len
+ ):
+ # SHA256 digest needs to be patched into this binary segment,
+ # calculate offset of the digest inside the binary segment.
+ patch_offset = self.elf_sha256_offset - file_pos
+ # Sanity checks
+ if (
+ patch_offset < self.SEG_HEADER_LEN
+ or patch_offset + self.SHA256_DIGEST_LEN > segment_len
+ ):
+ raise FatalError(
+ "Cannot place SHA256 digest on segment boundary"
+ "(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)"
+ % (self.elf_sha256_offset, file_pos, segment_len)
+ )
+ # offset relative to the data part
+ patch_offset -= self.SEG_HEADER_LEN
+ if (
+ segment_data[patch_offset : patch_offset + self.SHA256_DIGEST_LEN]
+ != b"\x00" * self.SHA256_DIGEST_LEN
+ ):
+ raise FatalError(
+ "Contents of segment at SHA256 digest offset 0x%x are not all zero."
+ " Refusing to overwrite." % self.elf_sha256_offset
+ )
+ assert len(self.elf_sha256) == self.SHA256_DIGEST_LEN
+ segment_data = (
+ segment_data[0:patch_offset]
+ + self.elf_sha256
+ + segment_data[patch_offset + self.SHA256_DIGEST_LEN :]
+ )
+ return segment_data
+
+ def save_segment(self, f, segment, checksum=None):
+ """
+ Save the next segment to the image file,
+ return next checksum value if provided
+ """
+ segment_data = self.maybe_patch_segment_data(f, segment.data)
+ f.write(struct.pack(" 0:
+ if len(irom_segments) != 1:
+ raise FatalError(
+ "Found %d segments that could be irom0. Bad ELF file?"
+ % len(irom_segments)
+ )
+ return irom_segments[0]
+ return None
+
+ def get_non_irom_segments(self):
+ irom_segment = self.get_irom_segment()
+ return [s for s in self.segments if s != irom_segment]
+
+ def merge_adjacent_segments(self):
+ if not self.segments:
+ return # nothing to merge
+
+ segments = []
+ # The easiest way to merge the sections is the browse them backward.
+ for i in range(len(self.segments) - 1, 0, -1):
+ # elem is the previous section, the one `next_elem` may need to be
+ # merged in
+ elem = self.segments[i - 1]
+ next_elem = self.segments[i]
+ if all(
+ (
+ elem.get_memory_type(self) == next_elem.get_memory_type(self),
+ elem.include_in_checksum == next_elem.include_in_checksum,
+ next_elem.addr == elem.addr + len(elem.data),
+ )
+ ):
+ # Merge any segment that ends where the next one starts,
+ # without spanning memory types
+ #
+ # (don't 'pad' any gaps here as they may be excluded from the image
+ # due to 'noinit' or other reasons.)
+ elem.data += next_elem.data
+ else:
+ # The section next_elem cannot be merged into the previous one,
+ # which means it needs to be part of the final segments.
+ # As we are browsing the list backward, the elements need to be
+ # inserted at the beginning of the final list.
+ segments.insert(0, next_elem)
+
+ # The first segment will always be here as it cannot be merged into any
+ # "previous" section.
+ segments.insert(0, self.segments[0])
+
+ # note: we could sort segments here as well, but the ordering of segments is
+ # sometimes important for other reasons (like embedded ELF SHA-256),
+ # so we assume that the linker script will have produced any adjacent sections
+ # in linear order in the ELF, anyhow.
+ self.segments = segments
+
+ def set_mmu_page_size(self, size):
+ """
+ If supported, this should be overridden by the chip-specific class.
+ Gets called in elf2image.
+ """
+ print(
+ "WARNING: Changing MMU page size is not supported on {}! "
+ "Defaulting to 64KB.".format(self.ROM_LOADER.CHIP_NAME)
+ )
+
+
+class ESP8266ROMFirmwareImage(BaseFirmwareImage):
+ """'Version 1' firmware image, segments loaded directly by the ROM bootloader."""
+
+ ROM_LOADER = ESP8266ROM
+
+ def __init__(self, load_file=None):
+ super(ESP8266ROMFirmwareImage, self).__init__()
+ self.flash_mode = 0
+ self.flash_size_freq = 0
+ self.version = 1
+
+ if load_file is not None:
+ segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
+
+ for _ in range(segments):
+ self.load_segment(load_file)
+ self.checksum = self.read_checksum(load_file)
+
+ self.verify()
+
+ def default_output_name(self, input_file):
+ """Derive a default output name from the ELF name."""
+ return input_file + "-"
+
+ def save(self, basename):
+ """Save a set of V1 images for flashing. Parameter is a base filename."""
+ # IROM data goes in its own plain binary file
+ irom_segment = self.get_irom_segment()
+ if irom_segment is not None:
+ with open(
+ "%s0x%05x.bin"
+ % (basename, irom_segment.addr - ESP8266ROM.IROM_MAP_START),
+ "wb",
+ ) as f:
+ f.write(irom_segment.data)
+
+ # everything but IROM goes at 0x00000 in an image file
+ normal_segments = self.get_non_irom_segments()
+ with open("%s0x00000.bin" % basename, "wb") as f:
+ self.write_common_header(f, normal_segments)
+ checksum = ESPLoader.ESP_CHECKSUM_MAGIC
+ for segment in normal_segments:
+ checksum = self.save_segment(f, segment, checksum)
+ self.append_checksum(f, checksum)
+
+
+ESP8266ROM.BOOTLOADER_IMAGE = ESP8266ROMFirmwareImage
+
+
+class ESP8266V2FirmwareImage(BaseFirmwareImage):
+ """'Version 2' firmware image, segments loaded by software bootloader stub
+ (ie Espressif bootloader or rboot)
+ """
+
+ ROM_LOADER = ESP8266ROM
+ # First byte of the "v2" application image
+ IMAGE_V2_MAGIC = 0xEA
+
+ # First 'segment' value in a "v2" application image,
+ # appears to be a constant version value?
+ IMAGE_V2_SEGMENT = 4
+
+ def __init__(self, load_file=None):
+ super(ESP8266V2FirmwareImage, self).__init__()
+ self.version = 2
+ if load_file is not None:
+ segments = self.load_common_header(load_file, self.IMAGE_V2_MAGIC)
+ if segments != self.IMAGE_V2_SEGMENT:
+ # segment count is not really segment count here,
+ # but we expect to see '4'
+ print(
+ 'Warning: V2 header has unexpected "segment" count %d (usually 4)'
+ % segments
+ )
+
+ # irom segment comes before the second header
+ #
+ # the file is saved in the image with a zero load address
+ # in the header, so we need to calculate a load address
+ irom_segment = self.load_segment(load_file, True)
+ # for actual mapped addr, add ESP8266ROM.IROM_MAP_START + flashing_addr + 8
+ irom_segment.addr = 0
+ irom_segment.include_in_checksum = False
+
+ first_flash_mode = self.flash_mode
+ first_flash_size_freq = self.flash_size_freq
+ first_entrypoint = self.entrypoint
+ # load the second header
+
+ segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
+
+ if first_flash_mode != self.flash_mode:
+ print(
+ "WARNING: Flash mode value in first header (0x%02x) disagrees "
+ "with second (0x%02x). Using second value."
+ % (first_flash_mode, self.flash_mode)
+ )
+ if first_flash_size_freq != self.flash_size_freq:
+ print(
+ "WARNING: Flash size/freq value in first header (0x%02x) disagrees "
+ "with second (0x%02x). Using second value."
+ % (first_flash_size_freq, self.flash_size_freq)
+ )
+ if first_entrypoint != self.entrypoint:
+ print(
+ "WARNING: Entrypoint address in first header (0x%08x) disagrees "
+ "with second header (0x%08x). Using second value."
+ % (first_entrypoint, self.entrypoint)
+ )
+
+ # load all the usual segments
+ for _ in range(segments):
+ self.load_segment(load_file)
+ self.checksum = self.read_checksum(load_file)
+
+ self.verify()
+
+ def default_output_name(self, input_file):
+ """Derive a default output name from the ELF name."""
+ irom_segment = self.get_irom_segment()
+ if irom_segment is not None:
+ irom_offs = irom_segment.addr - ESP8266ROM.IROM_MAP_START
+ else:
+ irom_offs = 0
+ return "%s-0x%05x.bin" % (
+ os.path.splitext(input_file)[0],
+ irom_offs & ~(ESPLoader.FLASH_SECTOR_SIZE - 1),
+ )
+
+ def save(self, filename):
+ with open(filename, "wb") as f:
+ # Save first header for irom0 segment
+ f.write(
+ struct.pack(
+ b" 0:
+ last_addr = flash_segments[0].addr
+ for segment in flash_segments[1:]:
+ if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN:
+ raise FatalError(
+ "Segment loaded at 0x%08x lands in same 64KB flash mapping "
+ "as segment loaded at 0x%08x. Can't generate binary. "
+ "Suggest changing linker script or ELF to merge sections."
+ % (segment.addr, last_addr)
+ )
+ last_addr = segment.addr
+
+ def get_alignment_data_needed(segment):
+ # Actual alignment (in data bytes) required for a segment header:
+ # positioned so that after we write the next 8 byte header,
+ # file_offs % IROM_ALIGN == segment.addr % IROM_ALIGN
+ #
+ # (this is because the segment's vaddr may not be IROM_ALIGNed,
+ # more likely is aligned IROM_ALIGN+0x18
+ # to account for the binary file header
+ align_past = (segment.addr % self.IROM_ALIGN) - self.SEG_HEADER_LEN
+ pad_len = (self.IROM_ALIGN - (f.tell() % self.IROM_ALIGN)) + align_past
+ if pad_len == 0 or pad_len == self.IROM_ALIGN:
+ return 0 # already aligned
+
+ # subtract SEG_HEADER_LEN a second time,
+ # as the padding block has a header as well
+ pad_len -= self.SEG_HEADER_LEN
+ if pad_len < 0:
+ pad_len += self.IROM_ALIGN
+ return pad_len
+
+ # try to fit each flash segment on a 64kB aligned boundary
+ # by padding with parts of the non-flash segments...
+ while len(flash_segments) > 0:
+ segment = flash_segments[0]
+ pad_len = get_alignment_data_needed(segment)
+ if pad_len > 0: # need to pad
+ if len(ram_segments) > 0 and pad_len > self.SEG_HEADER_LEN:
+ pad_segment = ram_segments[0].split_image(pad_len)
+ if len(ram_segments[0].data) == 0:
+ ram_segments.pop(0)
+ else:
+ pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell())
+ checksum = self.save_segment(f, pad_segment, checksum)
+ total_segments += 1
+ else:
+ # write the flash segment
+ assert (
+ f.tell() + 8
+ ) % self.IROM_ALIGN == segment.addr % self.IROM_ALIGN
+ checksum = self.save_flash_segment(f, segment, checksum)
+ flash_segments.pop(0)
+ total_segments += 1
+
+ # flash segments all written, so write any remaining RAM segments
+ for segment in ram_segments:
+ checksum = self.save_segment(f, segment, checksum)
+ total_segments += 1
+
+ if self.secure_pad:
+ # pad the image so that after signing it will end on a a 64KB boundary.
+ # This ensures all mapped flash content will be verified.
+ if not self.append_digest:
+ raise FatalError(
+ "secure_pad only applies if a SHA-256 digest "
+ "is also appended to the image"
+ )
+ align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN
+ # 16 byte aligned checksum
+ # (force the alignment to simplify calculations)
+ checksum_space = 16
+ if self.secure_pad == "1":
+ # after checksum: SHA-256 digest +
+ # (to be added by signing process) version,
+ # signature + 12 trailing bytes due to alignment
+ space_after_checksum = 32 + 4 + 64 + 12
+ elif self.secure_pad == "2": # Secure Boot V2
+ # after checksum: SHA-256 digest +
+ # signature sector,
+ # but we place signature sector after the 64KB boundary
+ space_after_checksum = 32
+ pad_len = (
+ self.IROM_ALIGN - align_past - checksum_space - space_after_checksum
+ ) % self.IROM_ALIGN
+ pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell())
+
+ checksum = self.save_segment(f, pad_segment, checksum)
+ total_segments += 1
+
+ # done writing segments
+ self.append_checksum(f, checksum)
+ image_length = f.tell()
+
+ if self.secure_pad:
+ assert ((image_length + space_after_checksum) % self.IROM_ALIGN) == 0
+
+ # kinda hacky: go back to the initial header and write the new segment count
+ # that includes padding segments. This header is not checksummed
+ f.seek(1)
+ f.write(bytes([total_segments]))
+
+ if self.append_digest:
+ # calculate the SHA256 of the whole file and append it
+ f.seek(0)
+ digest = hashlib.sha256()
+ digest.update(f.read(image_length))
+ f.write(digest.digest())
+
+ if self.pad_to_size:
+ image_length = f.tell()
+ if image_length % self.pad_to_size != 0:
+ pad_by = self.pad_to_size - (image_length % self.pad_to_size)
+ f.write(b"\xff" * pad_by)
+
+ with open(filename, "wb") as real_file:
+ real_file.write(f.getvalue())
+
+ def save_flash_segment(self, f, segment, checksum=None):
+ """
+ Save the next segment to the image file, return next checksum value if provided
+ """
+ segment_end_pos = f.tell() + len(segment.data) + self.SEG_HEADER_LEN
+ segment_len_remainder = segment_end_pos % self.IROM_ALIGN
+ if segment_len_remainder < 0x24:
+ # Work around a bug in ESP-IDF 2nd stage bootloader, that it didn't map the
+ # last MMU page, if an IROM/DROM segment was < 0x24 bytes
+ # over the page boundary.
+ segment.data += b"\x00" * (0x24 - segment_len_remainder)
+ return self.save_segment(f, segment, checksum)
+
+ def load_extended_header(self, load_file):
+ def split_byte(n):
+ return (n & 0x0F, (n >> 4) & 0x0F)
+
+ fields = list(
+ struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))
+ )
+
+ self.wp_pin = fields[0]
+
+ # SPI pin drive stengths are two per byte
+ self.clk_drv, self.q_drv = split_byte(fields[1])
+ self.d_drv, self.cs_drv = split_byte(fields[2])
+ self.hd_drv, self.wp_drv = split_byte(fields[3])
+
+ self.chip_id = fields[4]
+ if self.chip_id != self.ROM_LOADER.IMAGE_CHIP_ID:
+ print(
+ (
+ "Unexpected chip id in image. Expected %d but value was %d. "
+ "Is this image for a different chip model?"
+ )
+ % (self.ROM_LOADER.IMAGE_CHIP_ID, self.chip_id)
+ )
+
+ self.min_rev = fields[5]
+ self.min_rev_full = fields[6]
+ self.max_rev_full = fields[7]
+
+ # reserved fields in the middle should all be zero
+ if any(f for f in fields[8:-1] if f != 0):
+ print(
+ "Warning: some reserved header fields have non-zero values. "
+ "This image may be from a newer esptool.py?"
+ )
+
+ append_digest = fields[-1] # last byte is append_digest
+ if append_digest in [0, 1]:
+ self.append_digest = append_digest == 1
+ else:
+ raise RuntimeError(
+ "Invalid value for append_digest field (0x%02x). Should be 0 or 1.",
+ append_digest,
+ )
+
+ def save_extended_header(self, save_file):
+ def join_byte(ln, hn):
+ return (ln & 0x0F) + ((hn & 0x0F) << 4)
+
+ append_digest = 1 if self.append_digest else 0
+
+ fields = [
+ self.wp_pin,
+ join_byte(self.clk_drv, self.q_drv),
+ join_byte(self.d_drv, self.cs_drv),
+ join_byte(self.hd_drv, self.wp_drv),
+ self.ROM_LOADER.IMAGE_CHIP_ID,
+ self.min_rev,
+ self.min_rev_full,
+ self.max_rev_full,
+ ]
+ fields += [0] * 4 # padding
+ fields += [append_digest]
+
+ packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields)
+ save_file.write(packed)
+
+
+class ESP8266V3FirmwareImage(ESP32FirmwareImage):
+ """ESP8266 V3 firmware image is very similar to ESP32 image"""
+
+ EXTENDED_HEADER_STRUCT_FMT = "B" * 16
+
+ def is_flash_addr(self, addr):
+ return addr > ESP8266ROM.IROM_MAP_START
+
+ def save(self, filename):
+ total_segments = 0
+ with io.BytesIO() as f: # write file to memory first
+ self.write_common_header(f, self.segments)
+
+ checksum = ESPLoader.ESP_CHECKSUM_MAGIC
+
+ # split segments into flash-mapped vs ram-loaded,
+ # and take copies so we can mutate them
+ flash_segments = [
+ copy.deepcopy(s)
+ for s in sorted(self.segments, key=lambda s: s.addr)
+ if self.is_flash_addr(s.addr) and len(s.data)
+ ]
+ ram_segments = [
+ copy.deepcopy(s)
+ for s in sorted(self.segments, key=lambda s: s.addr)
+ if not self.is_flash_addr(s.addr) and len(s.data)
+ ]
+
+ # check for multiple ELF sections that are mapped in the same
+ # flash mapping region. This is usually a sign of a broken linker script,
+ # but if you have a legitimate use case then let us know
+ if len(flash_segments) > 0:
+ last_addr = flash_segments[0].addr
+ for segment in flash_segments[1:]:
+ if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN:
+ raise FatalError(
+ "Segment loaded at 0x%08x lands in same 64KB flash mapping "
+ "as segment loaded at 0x%08x. Can't generate binary. "
+ "Suggest changing linker script or ELF to merge sections."
+ % (segment.addr, last_addr)
+ )
+ last_addr = segment.addr
+
+ # try to fit each flash segment on a 64kB aligned boundary
+ # by padding with parts of the non-flash segments...
+ while len(flash_segments) > 0:
+ segment = flash_segments[0]
+ # remove 8 bytes empty data for insert segment header
+ if segment.name == ".flash.rodata":
+ segment.data = segment.data[8:]
+ # write the flash segment
+ checksum = self.save_segment(f, segment, checksum)
+ flash_segments.pop(0)
+ total_segments += 1
+
+ # flash segments all written, so write any remaining RAM segments
+ for segment in ram_segments:
+ checksum = self.save_segment(f, segment, checksum)
+ total_segments += 1
+
+ # done writing segments
+ self.append_checksum(f, checksum)
+ image_length = f.tell()
+
+ # kinda hacky: go back to the initial header and write the new segment count
+ # that includes padding segments. This header is not checksummed
+ f.seek(1)
+ f.write(bytes([total_segments]))
+
+ if self.append_digest:
+ # calculate the SHA256 of the whole file and append it
+ f.seek(0)
+ digest = hashlib.sha256()
+ digest.update(f.read(image_length))
+ f.write(digest.digest())
+
+ with open(filename, "wb") as real_file:
+ real_file.write(f.getvalue())
+
+ def load_extended_header(self, load_file):
+ def split_byte(n):
+ return (n & 0x0F, (n >> 4) & 0x0F)
+
+ fields = list(
+ struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))
+ )
+
+ self.wp_pin = fields[0]
+
+ # SPI pin drive stengths are two per byte
+ self.clk_drv, self.q_drv = split_byte(fields[1])
+ self.d_drv, self.cs_drv = split_byte(fields[2])
+ self.hd_drv, self.wp_drv = split_byte(fields[3])
+
+ if fields[15] in [0, 1]:
+ self.append_digest = fields[15] == 1
+ else:
+ raise RuntimeError(
+ "Invalid value for append_digest field (0x%02x). Should be 0 or 1.",
+ fields[15],
+ )
+
+ # remaining fields in the middle should all be zero
+ if any(f for f in fields[4:15] if f != 0):
+ print(
+ "Warning: some reserved header fields have non-zero values. "
+ "This image may be from a newer esptool.py?"
+ )
+
+
+ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage
+
+
+class ESP32S2FirmwareImage(ESP32FirmwareImage):
+ """ESP32S2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32S2ROM
+
+
+ESP32S2ROM.BOOTLOADER_IMAGE = ESP32S2FirmwareImage
+
+
+class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage):
+ """ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32S3BETA2ROM
+
+
+ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage
+
+
+class ESP32S3FirmwareImage(ESP32FirmwareImage):
+ """ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32S3ROM
+
+
+ESP32S3ROM.BOOTLOADER_IMAGE = ESP32S3FirmwareImage
+
+
+class ESP32C3FirmwareImage(ESP32FirmwareImage):
+ """ESP32C3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C3ROM
+
+
+ESP32C3ROM.BOOTLOADER_IMAGE = ESP32C3FirmwareImage
+
+
+class ESP32C6BETAFirmwareImage(ESP32FirmwareImage):
+ """ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C6BETAROM
+
+
+ESP32C6BETAROM.BOOTLOADER_IMAGE = ESP32C6BETAFirmwareImage
+
+
+class ESP32H2BETA1FirmwareImage(ESP32FirmwareImage):
+ """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32H2BETA1ROM
+
+
+ESP32H2BETA1ROM.BOOTLOADER_IMAGE = ESP32H2BETA1FirmwareImage
+
+
+class ESP32H2BETA2FirmwareImage(ESP32FirmwareImage):
+ """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32H2BETA2ROM
+
+
+ESP32H2BETA2ROM.BOOTLOADER_IMAGE = ESP32H2BETA2FirmwareImage
+
+
+class ESP32C2FirmwareImage(ESP32FirmwareImage):
+ """ESP32C2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C2ROM
+
+ def set_mmu_page_size(self, size):
+ if size not in [16384, 32768, 65536]:
+ raise FatalError(
+ "{} bytes is not a valid ESP32-C2 page size, "
+ "select from 64KB, 32KB, 16KB.".format(size)
+ )
+ self.IROM_ALIGN = size
+
+
+ESP32C2ROM.BOOTLOADER_IMAGE = ESP32C2FirmwareImage
+
+
+class ESP32C6FirmwareImage(ESP32FirmwareImage):
+ """ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C6ROM
+
+ def set_mmu_page_size(self, size):
+ if size not in [8192, 16384, 32768, 65536]:
+ raise FatalError(
+ "{} bytes is not a valid ESP32-C6 page size, "
+ "select from 64KB, 32KB, 16KB, 8KB.".format(size)
+ )
+ self.IROM_ALIGN = size
+
+
+ESP32C6ROM.BOOTLOADER_IMAGE = ESP32C6FirmwareImage
+
+
+class ESP32H2FirmwareImage(ESP32C6FirmwareImage):
+ """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32H2ROM
+
+
+ESP32H2ROM.BOOTLOADER_IMAGE = ESP32H2FirmwareImage
+
+
+class ELFFile(object):
+ SEC_TYPE_PROGBITS = 0x01
+ SEC_TYPE_STRTAB = 0x03
+ SEC_TYPE_INITARRAY = 0x0E
+ SEC_TYPE_FINIARRAY = 0x0F
+
+ PROG_SEC_TYPES = (SEC_TYPE_PROGBITS, SEC_TYPE_INITARRAY, SEC_TYPE_FINIARRAY)
+
+ LEN_SEC_HEADER = 0x28
+
+ SEG_TYPE_LOAD = 0x01
+ LEN_SEG_HEADER = 0x20
+
+ def __init__(self, name):
+ # Load sections from the ELF file
+ self.name = name
+ with open(self.name, "rb") as f:
+ self._read_elf_file(f)
+
+ def get_section(self, section_name):
+ for s in self.sections:
+ if s.name == section_name:
+ return s
+ raise ValueError("No section %s in ELF file" % section_name)
+
+ def _read_elf_file(self, f):
+ # read the ELF file header
+ LEN_FILE_HEADER = 0x34
+ try:
+ (
+ ident,
+ _type,
+ machine,
+ _version,
+ self.entrypoint,
+ _phoff,
+ shoff,
+ _flags,
+ _ehsize,
+ _phentsize,
+ _phnum,
+ shentsize,
+ shnum,
+ shstrndx,
+ ) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER))
+ except struct.error as e:
+ raise FatalError(
+ "Failed to read a valid ELF header from %s: %s" % (self.name, e)
+ )
+
+ if byte(ident, 0) != 0x7F or ident[1:4] != b"ELF":
+ raise FatalError("%s has invalid ELF magic header" % self.name)
+ if machine not in [0x5E, 0xF3]:
+ raise FatalError(
+ "%s does not appear to be an Xtensa or an RISCV ELF file. "
+ "e_machine=%04x" % (self.name, machine)
+ )
+ if shentsize != self.LEN_SEC_HEADER:
+ raise FatalError(
+ "%s has unexpected section header entry size 0x%x (not 0x%x)"
+ % (self.name, shentsize, self.LEN_SEC_HEADER)
+ )
+ if shnum == 0:
+ raise FatalError("%s has 0 section headers" % (self.name))
+ self._read_sections(f, shoff, shnum, shstrndx)
+ self._read_segments(f, _phoff, _phnum, shstrndx)
+
+ def _read_sections(self, f, section_header_offs, section_header_count, shstrndx):
+ f.seek(section_header_offs)
+ len_bytes = section_header_count * self.LEN_SEC_HEADER
+ section_header = f.read(len_bytes)
+ if len(section_header) == 0:
+ raise FatalError(
+ "No section header found at offset %04x in ELF file."
+ % section_header_offs
+ )
+ if len(section_header) != (len_bytes):
+ raise FatalError(
+ "Only read 0x%x bytes from section header (expected 0x%x.) "
+ "Truncated ELF file?" % (len(section_header), len_bytes)
+ )
+
+ # walk through the section header and extract all sections
+ section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER)
+
+ def read_section_header(offs):
+ name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from(
+ " 0
+ ]
+ self.sections = prog_sections
+
+ def _read_segments(self, f, segment_header_offs, segment_header_count, shstrndx):
+ f.seek(segment_header_offs)
+ len_bytes = segment_header_count * self.LEN_SEG_HEADER
+ segment_header = f.read(len_bytes)
+ if len(segment_header) == 0:
+ raise FatalError(
+ "No segment header found at offset %04x in ELF file."
+ % segment_header_offs
+ )
+ if len(segment_header) != (len_bytes):
+ raise FatalError(
+ "Only read 0x%x bytes from segment header (expected 0x%x.) "
+ "Truncated ELF file?" % (len(segment_header), len_bytes)
+ )
+
+ # walk through the segment header and extract all segments
+ segment_header_offsets = range(0, len(segment_header), self.LEN_SEG_HEADER)
+
+ def read_segment_header(offs):
+ (
+ seg_type,
+ seg_offs,
+ _vaddr,
+ lma,
+ size,
+ _memsize,
+ _flags,
+ _align,
+ ) = struct.unpack_from(" 0
+ ]
+ self.segments = prog_segments
+
+ def sha256(self):
+ # return SHA256 hash of the input ELF file
+ sha256 = hashlib.sha256()
+ with open(self.name, "rb") as f:
+ sha256.update(f.read())
+ return sha256.digest()
diff --git a/installer/bin/esptool/esptool/cmds.py b/installer/bin/esptool/esptool/cmds.py
new file mode 100644
index 0000000..a299058
--- /dev/null
+++ b/installer/bin/esptool/esptool/cmds.py
@@ -0,0 +1,1198 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import hashlib
+import io
+import os
+import struct
+import sys
+import time
+import zlib
+
+from .bin_image import ELFFile, ImageSegment, LoadFirmwareImage
+from .bin_image import (
+ ESP8266ROMFirmwareImage,
+ ESP8266V2FirmwareImage,
+ ESP8266V3FirmwareImage,
+)
+from .loader import (
+ DEFAULT_CONNECT_ATTEMPTS,
+ DEFAULT_TIMEOUT,
+ ERASE_WRITE_TIMEOUT_PER_MB,
+ ESPLoader,
+ timeout_per_mb,
+)
+from .targets import CHIP_DEFS, CHIP_LIST, ROM_LIST
+from .util import (
+ FatalError,
+ NotImplementedInROMError,
+ NotSupportedError,
+ UnsupportedCommandError,
+)
+from .util import (
+ div_roundup,
+ flash_size_bytes,
+ hexify,
+ pad_to,
+ print_overwrite,
+)
+
+DETECTED_FLASH_SIZES = {
+ 0x12: "256KB",
+ 0x13: "512KB",
+ 0x14: "1MB",
+ 0x15: "2MB",
+ 0x16: "4MB",
+ 0x17: "8MB",
+ 0x18: "16MB",
+ 0x19: "32MB",
+ 0x1A: "64MB",
+ 0x1B: "128MB",
+ 0x1C: "256MB",
+ 0x20: "64MB",
+ 0x21: "128MB",
+ 0x22: "256MB",
+ 0x32: "256KB",
+ 0x33: "512KB",
+ 0x34: "1MB",
+ 0x35: "2MB",
+ 0x36: "4MB",
+ 0x37: "8MB",
+ 0x38: "16MB",
+ 0x39: "32MB",
+ 0x3A: "64MB",
+}
+
+FLASH_MODES = {"qio": 0, "qout": 1, "dio": 2, "dout": 3}
+
+
+def detect_chip(
+ port=ESPLoader.DEFAULT_PORT,
+ baud=ESPLoader.ESP_ROM_BAUD,
+ connect_mode="default_reset",
+ trace_enabled=False,
+ connect_attempts=DEFAULT_CONNECT_ATTEMPTS,
+):
+ """Use serial access to detect the chip type.
+
+ First, get_security_info command is sent to detect the ID of the chip
+ (supported only by ESP32-C3 and later, works even in the Secure Download Mode).
+ If this fails, we reconnect and fall-back to reading the magic number.
+ It's mapped at a specific ROM address and has a different value on each chip model.
+ This way we use one memory read and compare it to the magic number for each chip.
+
+ This routine automatically performs ESPLoader.connect() (passing
+ connect_mode parameter) as part of querying the chip.
+ """
+ inst = None
+ detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled)
+ if detect_port.serial_port.startswith("rfc2217:"):
+ detect_port.USES_RFC2217 = True
+ detect_port.connect(connect_mode, connect_attempts, detecting=True)
+ try:
+ print("Detecting chip type...", end="")
+ chip_id = detect_port.get_chip_id()
+ for cls in [
+ n for n in ROM_LIST if n.CHIP_NAME not in ("ESP8266", "ESP32", "ESP32S2")
+ ]:
+ # cmd not supported on ESP8266 and ESP32 + ESP32-S2 doesn't return chip_id
+ if chip_id == cls.IMAGE_CHIP_ID:
+ inst = cls(detect_port._port, baud, trace_enabled=trace_enabled)
+ try:
+ inst.read_reg(
+ ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR
+ ) # Dummy read to check Secure Download mode
+ except UnsupportedCommandError:
+ inst.secure_download_mode = True
+ inst._post_connect()
+ break
+ else:
+ err_msg = f"Unexpected chip ID value {chip_id}."
+ except (UnsupportedCommandError, struct.error, FatalError) as e:
+ # UnsupportedCommmanddError: ESP8266/ESP32 ROM
+ # struct.error: ESP32-S2
+ # FatalError: ESP8266/ESP32 STUB
+ print(" Unsupported detection protocol, switching and trying again...")
+ try:
+ # ESP32/ESP8266 are reset after an unsupported command, need to reconnect
+ # (not needed on ESP32-S2)
+ if not isinstance(e, struct.error):
+ detect_port.connect(
+ connect_mode, connect_attempts, detecting=True, warnings=False
+ )
+ print("Detecting chip type...", end="")
+ sys.stdout.flush()
+ chip_magic_value = detect_port.read_reg(
+ ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR
+ )
+
+ for cls in ROM_LIST:
+ if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE:
+ inst = cls(detect_port._port, baud, trace_enabled=trace_enabled)
+ inst._post_connect()
+ inst.check_chip_id()
+ break
+ else:
+ err_msg = f"Unexpected chip magic value {chip_magic_value:#010x}."
+ except UnsupportedCommandError:
+ raise FatalError(
+ "Unsupported Command Error received. "
+ "Probably this means Secure Download Mode is enabled, "
+ "autodetection will not work. Need to manually specify the chip."
+ )
+ finally:
+ if inst is not None:
+ print(" %s" % inst.CHIP_NAME, end="")
+ if detect_port.sync_stub_detected:
+ inst = inst.STUB_CLASS(inst)
+ inst.sync_stub_detected = True
+ print("") # end line
+ return inst
+ raise FatalError(
+ f"{err_msg} Failed to autodetect chip type."
+ "\nProbably it is unsupported by this version of esptool."
+ )
+
+
+# "Operation" commands, executable at command line. One function each
+#
+# Each function takes either two args (, ) or a single
+# argument.
+
+
+def load_ram(esp, args):
+ image = LoadFirmwareImage(esp.CHIP_NAME, args.filename)
+
+ print("RAM boot...")
+ for seg in image.segments:
+ size = len(seg.data)
+ print("Downloading %d bytes at %08x..." % (size, seg.addr), end=" ")
+ sys.stdout.flush()
+ esp.mem_begin(
+ size, div_roundup(size, esp.ESP_RAM_BLOCK), esp.ESP_RAM_BLOCK, seg.addr
+ )
+
+ seq = 0
+ while len(seg.data) > 0:
+ esp.mem_block(seg.data[0 : esp.ESP_RAM_BLOCK], seq)
+ seg.data = seg.data[esp.ESP_RAM_BLOCK :]
+ seq += 1
+ print("done!")
+
+ print("All segments done, executing at %08x" % image.entrypoint)
+ esp.mem_finish(image.entrypoint)
+
+
+def read_mem(esp, args):
+ print("0x%08x = 0x%08x" % (args.address, esp.read_reg(args.address)))
+
+
+def write_mem(esp, args):
+ esp.write_reg(args.address, args.value, args.mask, 0)
+ print("Wrote %08x, mask %08x to %08x" % (args.value, args.mask, args.address))
+
+
+def dump_mem(esp, args):
+ with open(args.filename, "wb") as f:
+ for i in range(args.size // 4):
+ d = esp.read_reg(args.address + (i * 4))
+ f.write(struct.pack(b"> 16
+ args.flash_size = DETECTED_FLASH_SIZES.get(size_id)
+ if args.flash_size is None:
+ print(
+ "Warning: Could not auto-detect Flash size (FlashID=0x%x, SizeID=0x%x),"
+ " defaulting to 4MB" % (flash_id, size_id)
+ )
+ args.flash_size = "4MB"
+ else:
+ print("Auto-detected Flash size:", args.flash_size)
+
+
+def _update_image_flash_params(esp, address, args, image):
+ """
+ Modify the flash mode & size bytes if this looks like an executable bootloader image
+ """
+ if len(image) < 8:
+ return image # not long enough to be a bootloader image
+
+ # unpack the (potential) image header
+ magic, _, flash_mode, flash_size_freq = struct.unpack("BBBB", image[:4])
+ if address != esp.BOOTLOADER_FLASH_OFFSET:
+ return image # not flashing bootloader offset, so don't modify this
+
+ if (args.flash_mode, args.flash_freq, args.flash_size) == ("keep",) * 3:
+ return image # all settings are 'keep', not modifying anything
+
+ # easy check if this is an image: does it start with a magic byte?
+ if magic != esp.ESP_IMAGE_MAGIC:
+ print(
+ "Warning: Image file at 0x%x doesn't look like an image file, "
+ "so not changing any flash settings." % address
+ )
+ return image
+
+ # make sure this really is an image, and not just data that
+ # starts with esp.ESP_IMAGE_MAGIC (mostly a problem for encrypted
+ # images that happen to start with a magic byte
+ try:
+ test_image = esp.BOOTLOADER_IMAGE(io.BytesIO(image))
+ test_image.verify()
+ except Exception:
+ print(
+ "Warning: Image file at 0x%x is not a valid %s image, "
+ "so not changing any flash settings." % (address, esp.CHIP_NAME)
+ )
+ return image
+
+ # After the 8-byte header comes the extended header for chips others than ESP8266.
+ # The 15th byte of the extended header indicates if the image is protected by
+ # a SHA256 checksum. In that case we should not modify the header because
+ # the checksum check would fail.
+ sha_implies_keep = args.chip != "esp8266" and image[8 + 15] == 1
+
+ def print_keep_warning(arg_to_keep, arg_used):
+ print(
+ "Warning: Image file at {addr} is protected with a hash checksum, "
+ "so not changing the flash {arg} setting. "
+ "Use the --flash_{arg}=keep option instead of --flash_{arg}={arg_orig} "
+ "in order to remove this warning, or use the --dont-append-digest option "
+ "for the elf2image command in order to generate an image file "
+ "without a hash checksum".format(
+ addr=hex(address), arg=arg_to_keep, arg_orig=arg_used
+ )
+ )
+
+ if args.flash_mode != "keep":
+ new_flash_mode = FLASH_MODES[args.flash_mode]
+ if flash_mode != new_flash_mode and sha_implies_keep:
+ print_keep_warning("mode", args.flash_mode)
+ else:
+ flash_mode = new_flash_mode
+
+ flash_freq = flash_size_freq & 0x0F
+ if args.flash_freq != "keep":
+ new_flash_freq = esp.parse_flash_freq_arg(args.flash_freq)
+ if flash_freq != new_flash_freq and sha_implies_keep:
+ print_keep_warning("frequency", args.flash_freq)
+ else:
+ flash_freq = new_flash_freq
+
+ flash_size = flash_size_freq & 0xF0
+ if args.flash_size != "keep":
+ new_flash_size = esp.parse_flash_size_arg(args.flash_size)
+ if flash_size != new_flash_size and sha_implies_keep:
+ print_keep_warning("size", args.flash_size)
+ else:
+ flash_size = new_flash_size
+
+ flash_params = struct.pack(b"BB", flash_mode, flash_size + flash_freq)
+ if flash_params != image[2:4]:
+ print("Flash params set to 0x%04x" % struct.unpack(">H", flash_params))
+ image = image[0:2] + flash_params + image[4:]
+ return image
+
+
+def write_flash(esp, args):
+ # set args.compress based on default behaviour:
+ # -> if either --compress or --no-compress is set, honour that
+ # -> otherwise, set --compress unless --no-stub is set
+ if args.compress is None and not args.no_compress:
+ args.compress = not args.no_stub
+
+ if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode:
+ # Check if secure boot is active
+ if esp.get_secure_boot_enabled():
+ for address, _ in args.addr_filename:
+ if address < 0x8000:
+ raise FatalError(
+ "Secure Boot detected, writing to flash regions < 0x8000 "
+ "is disabled to protect the bootloader. "
+ "Use --force to override, "
+ "please use with caution, otherwise it may brick your device!"
+ )
+ # Check if chip_id and min_rev in image are valid for the target in use
+ for _, argfile in args.addr_filename:
+ try:
+ image = LoadFirmwareImage(esp.CHIP_NAME, argfile)
+ except (FatalError, struct.error, RuntimeError):
+ continue
+ finally:
+ argfile.seek(0) # LoadFirmwareImage changes the file handle position
+ if image.chip_id != esp.IMAGE_CHIP_ID:
+ raise FatalError(
+ f"{argfile.name} is not an {esp.CHIP_NAME} image. "
+ "Use --force to flash anyway."
+ )
+
+ # this logic below decides which min_rev to use, min_rev or min/max_rev_full
+ if image.max_rev_full == 0: # image does not have max/min_rev_full fields
+ use_rev_full_fields = False
+ elif image.max_rev_full == 65535: # image has default value of max_rev_full
+ use_rev_full_fields = True
+ if (
+ image.min_rev_full == 0 and image.min_rev != 0
+ ): # min_rev_full is not set, min_rev is used
+ use_rev_full_fields = False
+ else: # max_rev_full set to a version
+ use_rev_full_fields = True
+
+ if use_rev_full_fields:
+ rev = esp.get_chip_revision()
+ if rev < image.min_rev_full or rev > image.max_rev_full:
+ error_str = f"{argfile.name} requires chip revision in range "
+ error_str += (
+ f"[v{image.min_rev_full // 100}.{image.min_rev_full % 100} - "
+ )
+ if image.max_rev_full == 65535:
+ error_str += "max rev not set] "
+ else:
+ error_str += (
+ f"v{image.max_rev_full // 100}.{image.max_rev_full % 100}] "
+ )
+ error_str += f"(this chip is revision v{rev // 100}.{rev % 100})"
+ raise FatalError(f"{error_str}. Use --force to flash anyway.")
+ else:
+ # In IDF, image.min_rev is set based on Kconfig option.
+ # For C3 chip, image.min_rev is the Minor revision
+ # while for the rest chips it is the Major revision.
+ if esp.CHIP_NAME == "ESP32-C3":
+ rev = esp.get_minor_chip_version()
+ else:
+ rev = esp.get_major_chip_version()
+ if rev < image.min_rev:
+ raise FatalError(
+ f"{argfile.name} requires chip revision "
+ f"{image.min_rev} or higher (this chip is revision {rev}). "
+ "Use --force to flash anyway."
+ )
+
+ # In case we have encrypted files to write,
+ # we first do few sanity checks before actual flash
+ if args.encrypt or args.encrypt_files is not None:
+ do_write = True
+
+ if not esp.secure_download_mode:
+ if esp.get_encrypted_download_disabled():
+ raise FatalError(
+ "This chip has encrypt functionality "
+ "in UART download mode disabled. "
+ "This is the Flash Encryption configuration for Production mode "
+ "instead of Development mode."
+ )
+
+ crypt_cfg_efuse = esp.get_flash_crypt_config()
+
+ if crypt_cfg_efuse is not None and crypt_cfg_efuse != 0xF:
+ print("Unexpected FLASH_CRYPT_CONFIG value: 0x%x" % (crypt_cfg_efuse))
+ do_write = False
+
+ enc_key_valid = esp.is_flash_encryption_key_valid()
+
+ if not enc_key_valid:
+ print("Flash encryption key is not programmed")
+ do_write = False
+
+ # Determine which files list contain the ones to encrypt
+ files_to_encrypt = args.addr_filename if args.encrypt else args.encrypt_files
+
+ for address, argfile in files_to_encrypt:
+ if address % esp.FLASH_ENCRYPTED_WRITE_ALIGN:
+ print(
+ "File %s address 0x%x is not %d byte aligned, can't flash encrypted"
+ % (argfile.name, address, esp.FLASH_ENCRYPTED_WRITE_ALIGN)
+ )
+ do_write = False
+
+ if not do_write and not args.ignore_flash_encryption_efuse_setting:
+ raise FatalError(
+ "Can't perform encrypted flash write, "
+ "consult Flash Encryption documentation for more information"
+ )
+ else:
+ if not args.force and esp.CHIP_NAME != "ESP8266":
+ # ESP32 does not support `get_security_info()` and `secure_download_mode`
+ if (
+ esp.CHIP_NAME != "ESP32"
+ and esp.secure_download_mode
+ and bin(esp.get_security_info()["flash_crypt_cnt"]).count("1") & 1 != 0
+ ):
+ raise FatalError(
+ "WARNING: Detected flash encryption and "
+ "secure download mode enabled.\n"
+ "Flashing plaintext binary may brick your device! "
+ "Use --force to override the warning."
+ )
+
+ if (
+ not esp.secure_download_mode
+ and esp.get_encrypted_download_disabled()
+ and esp.get_flash_encryption_enabled()
+ ):
+ raise FatalError(
+ "WARNING: Detected flash encryption enabled and "
+ "download manual encrypt disabled.\n"
+ "Flashing plaintext binary may brick your device! "
+ "Use --force to override the warning."
+ )
+
+ # verify file sizes fit in flash
+ if args.flash_size != "keep": # TODO: check this even with 'keep'
+ flash_end = flash_size_bytes(args.flash_size)
+ for address, argfile in args.addr_filename:
+ argfile.seek(0, os.SEEK_END)
+ if address + argfile.tell() > flash_end:
+ raise FatalError(
+ "File %s (length %d) at offset %d "
+ "will not fit in %d bytes of flash. "
+ "Use --flash_size argument, or change flashing address."
+ % (argfile.name, argfile.tell(), address, flash_end)
+ )
+ argfile.seek(0)
+
+ if args.erase_all:
+ erase_flash(esp, args)
+ else:
+ for address, argfile in args.addr_filename:
+ argfile.seek(0, os.SEEK_END)
+ write_end = address + argfile.tell()
+ argfile.seek(0)
+ bytes_over = address % esp.FLASH_SECTOR_SIZE
+ if bytes_over != 0:
+ print(
+ "WARNING: Flash address {:#010x} is not aligned "
+ "to a {:#x} byte flash sector. "
+ "{:#x} bytes before this address will be erased.".format(
+ address, esp.FLASH_SECTOR_SIZE, bytes_over
+ )
+ )
+ # Print the address range of to-be-erased flash memory region
+ print(
+ "Flash will be erased from {:#010x} to {:#010x}...".format(
+ address - bytes_over,
+ div_roundup(write_end, esp.FLASH_SECTOR_SIZE)
+ * esp.FLASH_SECTOR_SIZE
+ - 1,
+ )
+ )
+
+ """ Create a list describing all the files we have to flash.
+ Each entry holds an "encrypt" flag marking whether the file needs encryption or not.
+ This list needs to be sorted.
+
+ First, append to each entry of our addr_filename list the flag args.encrypt
+ E.g., if addr_filename is [(0x1000, "partition.bin"), (0x8000, "bootloader")],
+ all_files will be [
+ (0x1000, "partition.bin", args.encrypt),
+ (0x8000, "bootloader", args.encrypt)
+ ],
+ where, of course, args.encrypt is either True or False
+ """
+ all_files = [
+ (offs, filename, args.encrypt) for (offs, filename) in args.addr_filename
+ ]
+
+ """
+ Now do the same with encrypt_files list, if defined.
+ In this case, the flag is True
+ """
+ if args.encrypt_files is not None:
+ encrypted_files_flag = [
+ (offs, filename, True) for (offs, filename) in args.encrypt_files
+ ]
+
+ # Concatenate both lists and sort them.
+ # As both list are already sorted, we could simply do a merge instead,
+ # but for the sake of simplicity and because the lists are very small,
+ # let's use sorted.
+ all_files = sorted(all_files + encrypted_files_flag, key=lambda x: x[0])
+
+ for address, argfile, encrypted in all_files:
+ compress = args.compress
+
+ # Check whether we can compress the current file before flashing
+ if compress and encrypted:
+ print("\nWARNING: - compress and encrypt options are mutually exclusive ")
+ print("Will flash %s uncompressed" % argfile.name)
+ compress = False
+
+ if args.no_stub:
+ print("Erasing flash...")
+ image = pad_to(
+ argfile.read(), esp.FLASH_ENCRYPTED_WRITE_ALIGN if encrypted else 4
+ )
+ if len(image) == 0:
+ print("WARNING: File %s is empty" % argfile.name)
+ continue
+ image = _update_image_flash_params(esp, address, args, image)
+ calcmd5 = hashlib.md5(image).hexdigest()
+ uncsize = len(image)
+ if compress:
+ uncimage = image
+ image = zlib.compress(uncimage, 9)
+ # Decompress the compressed binary a block at a time,
+ # to dynamically calculate the timeout based on the real write size
+ decompress = zlib.decompressobj()
+ blocks = esp.flash_defl_begin(uncsize, len(image), address)
+ else:
+ blocks = esp.flash_begin(uncsize, address, begin_rom_encrypted=encrypted)
+ argfile.seek(0) # in case we need it again
+ seq = 0
+ bytes_sent = 0 # bytes sent on wire
+ bytes_written = 0 # bytes written to flash
+ t = time.time()
+
+ timeout = DEFAULT_TIMEOUT
+
+ while len(image) > 0:
+ print_overwrite(
+ "Writing at 0x%08x... (%d %%)"
+ % (address + bytes_written, 100 * (seq + 1) // blocks)
+ )
+ sys.stdout.flush()
+ block = image[0 : esp.FLASH_WRITE_SIZE]
+ if compress:
+ # feeding each compressed block into the decompressor lets us
+ # see block-by-block how much will be written
+ block_uncompressed = len(decompress.decompress(block))
+ bytes_written += block_uncompressed
+ block_timeout = max(
+ DEFAULT_TIMEOUT,
+ timeout_per_mb(ERASE_WRITE_TIMEOUT_PER_MB, block_uncompressed),
+ )
+ if not esp.IS_STUB:
+ timeout = (
+ block_timeout # ROM code writes block to flash before ACKing
+ )
+ esp.flash_defl_block(block, seq, timeout=timeout)
+ if esp.IS_STUB:
+ # Stub ACKs when block is received,
+ # then writes to flash while receiving the block after it
+ timeout = block_timeout
+ else:
+ # Pad the last block
+ block = block + b"\xff" * (esp.FLASH_WRITE_SIZE - len(block))
+ if encrypted:
+ esp.flash_encrypt_block(block, seq)
+ else:
+ esp.flash_block(block, seq)
+ bytes_written += len(block)
+ bytes_sent += len(block)
+ image = image[esp.FLASH_WRITE_SIZE :]
+ seq += 1
+
+ if esp.IS_STUB:
+ # Stub only writes each block to flash after 'ack'ing the receive,
+ # so do a final dummy operation which will not be 'ack'ed
+ # until the last block has actually been written out to flash
+ esp.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR, timeout=timeout)
+
+ t = time.time() - t
+ speed_msg = ""
+ if compress:
+ if t > 0.0:
+ speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000)
+ print_overwrite(
+ "Wrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s..."
+ % (uncsize, bytes_sent, address, t, speed_msg),
+ last_line=True,
+ )
+ else:
+ if t > 0.0:
+ speed_msg = " (%.1f kbit/s)" % (bytes_written / t * 8 / 1000)
+ print_overwrite(
+ "Wrote %d bytes at 0x%08x in %.1f seconds%s..."
+ % (bytes_written, address, t, speed_msg),
+ last_line=True,
+ )
+
+ if not encrypted and not esp.secure_download_mode:
+ try:
+ res = esp.flash_md5sum(address, uncsize)
+ if res != calcmd5:
+ print("File md5: %s" % calcmd5)
+ print("Flash md5: %s" % res)
+ print(
+ "MD5 of 0xFF is %s"
+ % (hashlib.md5(b"\xFF" * uncsize).hexdigest())
+ )
+ raise FatalError("MD5 of file does not match data in flash!")
+ else:
+ print("Hash of data verified.")
+ except NotImplementedInROMError:
+ pass
+
+ print("\nLeaving...")
+
+ if esp.IS_STUB:
+ # skip sending flash_finish to ROM loader here,
+ # as it causes the loader to exit and run user code
+ esp.flash_begin(0, 0)
+
+ # Get the "encrypted" flag for the last file flashed
+ # Note: all_files list contains triplets like:
+ # (address: Integer, filename: String, encrypted: Boolean)
+ last_file_encrypted = all_files[-1][2]
+
+ # Check whether the last file flashed was compressed or not
+ if args.compress and not last_file_encrypted:
+ esp.flash_defl_finish(False)
+ else:
+ esp.flash_finish(False)
+
+ if args.verify:
+ print("Verifying just-written flash...")
+ print(
+ "(This option is deprecated, "
+ "flash contents are now always read back after flashing.)"
+ )
+ # If some encrypted files have been flashed,
+ # print a warning saying that we won't check them
+ if args.encrypt or args.encrypt_files is not None:
+ print("WARNING: - cannot verify encrypted files, they will be ignored")
+ # Call verify_flash function only if there is at least
+ # one non-encrypted file flashed
+ if not args.encrypt:
+ verify_flash(esp, args)
+
+
+def image_info(args):
+ def v2():
+ def get_key_from_value(dict, val):
+ """Get key from value in dictionary"""
+ for key, value in dict.items():
+ if value == val:
+ return key
+ return None
+
+ print()
+ title = "{} image header".format(args.chip.upper())
+ print(title)
+ print("=" * len(title))
+ print("Image version: {}".format(image.version))
+ print(
+ "Entry point: {:#8x}".format(image.entrypoint)
+ if image.entrypoint != 0
+ else "Entry point not set"
+ )
+
+ print("Segments: {}".format(len(image.segments)))
+
+ # Flash size
+ flash_s_bits = image.flash_size_freq & 0xF0 # high four bits
+ flash_s = get_key_from_value(image.ROM_LOADER.FLASH_SIZES, flash_s_bits)
+ print(
+ "Flash size: {}".format(flash_s)
+ if flash_s is not None
+ else "WARNING: Invalid flash size ({:#02x})".format(flash_s_bits)
+ )
+
+ # Flash frequency
+ flash_fr_bits = image.flash_size_freq & 0x0F # low four bits
+ flash_fr = get_key_from_value(image.ROM_LOADER.FLASH_FREQUENCY, flash_fr_bits)
+ print(
+ "Flash freq: {}".format(flash_fr)
+ if flash_fr is not None
+ else "WARNING: Invalid flash frequency ({:#02x})".format(flash_fr_bits)
+ )
+
+ # Flash mode
+ flash_mode = get_key_from_value(FLASH_MODES, image.flash_mode)
+ print(
+ "Flash mode: {}".format(flash_mode.upper())
+ if flash_mode is not None
+ else "WARNING: Invalid flash mode ({})".format(image.flash_mode)
+ )
+
+ # Extended header (ESP32 and later only)
+ if args.chip != "esp8266":
+ print()
+ title = "{} extended image header".format(args.chip.upper())
+ print(title)
+ print("=" * len(title))
+ print("WP pin: {:#02x}".format(image.wp_pin))
+ print(
+ "Flash pins drive settings: "
+ "clk_drv: {:#02x}, q_drv: {:#02x}, d_drv: {:#02x}, "
+ "cs0_drv: {:#02x}, hd_drv: {:#02x}, wp_drv: {:#02x}".format(
+ image.clk_drv,
+ image.q_drv,
+ image.d_drv,
+ image.cs_drv,
+ image.hd_drv,
+ image.wp_drv,
+ )
+ )
+ print("Chip ID: {}".format(image.chip_id))
+ print(
+ "Minimal chip revision: "
+ f"v{image.min_rev_full // 100}.{image.min_rev_full % 100}, "
+ f"(legacy min_rev = {image.min_rev})"
+ )
+ print(
+ "Maximal chip revision: "
+ f"v{image.max_rev_full // 100}.{image.max_rev_full % 100}"
+ )
+ print()
+
+ # Segments overview
+ title = "Segments information"
+ print(title)
+ print("=" * len(title))
+ headers_str = "{:>7} {:>7} {:>10} {:>10} {:10}"
+ print(
+ headers_str.format(
+ "Segment", "Length", "Load addr", "File offs", "Memory types"
+ )
+ )
+ print(
+ "{} {} {} {} {}".format("-" * 7, "-" * 7, "-" * 10, "-" * 10, "-" * 12)
+ )
+ format_str = "{:7} {:#07x} {:#010x} {:#010x} {}"
+ app_desc = None
+ for idx, seg in enumerate(image.segments, start=1):
+ segs = seg.get_memory_type(image)
+ seg_name = ", ".join(segs)
+ if "DROM" in segs: # The DROM segment starts with the esp_app_desc_t struct
+ app_desc = seg.data[:256]
+ print(
+ format_str.format(idx, len(seg.data), seg.addr, seg.file_offs, seg_name)
+ )
+ print()
+
+ # Footer
+ title = f"{args.chip.upper()} image footer"
+ print(title)
+ print("=" * len(title))
+ calc_checksum = image.calculate_checksum()
+ print(
+ "Checksum: {:#02x} ({})".format(
+ image.checksum,
+ "valid"
+ if image.checksum == calc_checksum
+ else "invalid - calculated {:02x}".format(calc_checksum),
+ )
+ )
+ try:
+ digest_msg = "Not appended"
+ if image.append_digest:
+ is_valid = image.stored_digest == image.calc_digest
+ digest_msg = "{} ({})".format(
+ hexify(image.calc_digest, uppercase=False),
+ "valid" if is_valid else "invalid",
+ )
+ print("Validation hash: {}".format(digest_msg))
+ except AttributeError:
+ pass # ESP8266 image has no append_digest field
+
+ if app_desc:
+ APP_DESC_STRUCT_FMT = " 1 else ""))
+
+ image.verify()
+
+ if args.output is None:
+ args.output = image.default_output_name(args.input)
+ image.save(args.output)
+
+ print("Successfully created {} image.".format(args.chip))
+
+
+def read_mac(esp, args):
+ mac = esp.read_mac()
+
+ def print_mac(label, mac):
+ print("%s: %s" % (label, ":".join(map(lambda x: "%02x" % x, mac))))
+
+ print_mac("MAC", mac)
+
+
+def chip_id(esp, args):
+ try:
+ chipid = esp.chip_id()
+ print("Chip ID: 0x%08x" % chipid)
+ except NotSupportedError:
+ print("Warning: %s has no Chip ID. Reading MAC instead." % esp.CHIP_NAME)
+ read_mac(esp, args)
+
+
+def erase_flash(esp, args):
+ if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode:
+ if esp.get_flash_encryption_enabled() or esp.get_secure_boot_enabled():
+ raise FatalError(
+ "Active security features detected, "
+ "erasing flash is disabled as a safety measure. "
+ "Use --force to override, "
+ "please use with caution, otherwise it may brick your device!"
+ )
+ print("Erasing flash (this may take a while)...")
+ t = time.time()
+ esp.erase_flash()
+ print("Chip erase completed successfully in %.1fs" % (time.time() - t))
+
+
+def erase_region(esp, args):
+ if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode:
+ if esp.get_flash_encryption_enabled() or esp.get_secure_boot_enabled():
+ raise FatalError(
+ "Active security features detected, "
+ "erasing flash is disabled as a safety measure. "
+ "Use --force to override, "
+ "please use with caution, otherwise it may brick your device!"
+ )
+ print("Erasing region (may be slow depending on size)...")
+ t = time.time()
+ esp.erase_region(args.address, args.size)
+ print("Erase completed successfully in %.1f seconds." % (time.time() - t))
+
+
+def run(esp, args):
+ esp.run()
+
+
+def flash_id(esp, args):
+ flash_id = esp.flash_id()
+ print("Manufacturer: %02x" % (flash_id & 0xFF))
+ flid_lowbyte = (flash_id >> 16) & 0xFF
+ print("Device: %02x%02x" % ((flash_id >> 8) & 0xFF, flid_lowbyte))
+ print(
+ "Detected flash size: %s" % (DETECTED_FLASH_SIZES.get(flid_lowbyte, "Unknown"))
+ )
+ flash_type = esp.flash_type()
+ flash_type_dict = {0: "quad (4 data lines)", 1: "octal (8 data lines)"}
+ flash_type_str = flash_type_dict.get(flash_type)
+ if flash_type_str:
+ print(f"Flash type set in eFuse: {flash_type_str}")
+
+
+def read_flash(esp, args):
+ if args.no_progress:
+ flash_progress = None
+ else:
+
+ def flash_progress(progress, length):
+ msg = "%d (%d %%)" % (progress, progress * 100.0 / length)
+ padding = "\b" * len(msg)
+ if progress == length:
+ padding = "\n"
+ sys.stdout.write(msg + padding)
+ sys.stdout.flush()
+
+ t = time.time()
+ data = esp.read_flash(args.address, args.size, flash_progress)
+ t = time.time() - t
+ speed_msg = " ({:.1f} kbit/s)".format(len(data) / t * 8 / 1000) if t > 0.0 else ""
+ print_overwrite(
+ "Read {:d} bytes at {:#010x} in {:.1f} seconds{}...".format(
+ len(data), args.address, t, speed_msg
+ ),
+ last_line=True,
+ )
+ with open(args.filename, "wb") as f:
+ f.write(data)
+
+
+def verify_flash(esp, args):
+ differences = False
+
+ for address, argfile in args.addr_filename:
+ image = pad_to(argfile.read(), 4)
+ argfile.seek(0) # rewind in case we need it again
+
+ image = _update_image_flash_params(esp, address, args, image)
+
+ image_size = len(image)
+ print(
+ "Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s..."
+ % (image_size, image_size, address, argfile.name)
+ )
+ # Try digest first, only read if there are differences.
+ digest = esp.flash_md5sum(address, image_size)
+ expected_digest = hashlib.md5(image).hexdigest()
+ if digest == expected_digest:
+ print("-- verify OK (digest matched)")
+ continue
+ else:
+ differences = True
+ if getattr(args, "diff", "no") != "yes":
+ print("-- verify FAILED (digest mismatch)")
+ continue
+
+ flash = esp.read_flash(address, image_size)
+ assert flash != image
+ diff = [i for i in range(image_size) if flash[i] != image[i]]
+ print(
+ "-- verify FAILED: %d differences, first @ 0x%08x"
+ % (len(diff), address + diff[0])
+ )
+ for d in diff:
+ flash_byte = flash[d]
+ image_byte = image[d]
+ print(" %08x %02x %02x" % (address + d, flash_byte, image_byte))
+ if differences:
+ raise FatalError("Verify failed.")
+
+
+def read_flash_status(esp, args):
+ print("Status value: 0x%04x" % esp.read_status(args.bytes))
+
+
+def write_flash_status(esp, args):
+ fmt = "0x%%0%dx" % (args.bytes * 2)
+ args.value = args.value & ((1 << (args.bytes * 8)) - 1)
+ print(("Initial flash status: " + fmt) % esp.read_status(args.bytes))
+ print(("Setting flash status: " + fmt) % args.value)
+ esp.write_status(args.value, args.bytes, args.non_volatile)
+ print(("After flash status: " + fmt) % esp.read_status(args.bytes))
+
+
+def get_security_info(esp, args):
+ si = esp.get_security_info()
+ # TODO: better display
+ print("Flags: {:#010x} ({})".format(si["flags"], bin(si["flags"])))
+ print("Flash_Crypt_Cnt: {:#x}".format(si["flash_crypt_cnt"]))
+ print("Key_Purposes: {}".format(si["key_purposes"]))
+ if si["chip_id"] is not None and si["api_version"] is not None:
+ print("Chip_ID: {}".format(si["chip_id"]))
+ print("Api_Version: {}".format(si["api_version"]))
+
+
+def merge_bin(args):
+ try:
+ chip_class = CHIP_DEFS[args.chip]
+ except KeyError:
+ msg = (
+ "Please specify the chip argument"
+ if args.chip == "auto"
+ else "Invalid chip choice: '{}'".format(args.chip)
+ )
+ msg = msg + " (choose from {})".format(", ".join(CHIP_LIST))
+ raise FatalError(msg)
+
+ # sort the files by offset.
+ # The AddrFilenamePairAction has already checked for overlap
+ input_files = sorted(args.addr_filename, key=lambda x: x[0])
+ if not input_files:
+ raise FatalError("No input files specified")
+ first_addr = input_files[0][0]
+ if first_addr < args.target_offset:
+ raise FatalError(
+ "Output file target offset is 0x%x. Input file offset 0x%x is before this."
+ % (args.target_offset, first_addr)
+ )
+
+ if args.format != "raw":
+ raise FatalError(
+ "This version of esptool only supports the 'raw' output format"
+ )
+
+ with open(args.output, "wb") as of:
+
+ def pad_to(flash_offs):
+ # account for output file offset if there is any
+ of.write(b"\xFF" * (flash_offs - args.target_offset - of.tell()))
+
+ for addr, argfile in input_files:
+ pad_to(addr)
+ image = argfile.read()
+ image = _update_image_flash_params(chip_class, addr, args, image)
+ of.write(image)
+ if args.fill_flash_size:
+ pad_to(flash_size_bytes(args.fill_flash_size))
+ print(
+ "Wrote 0x%x bytes to file %s, ready to flash to offset 0x%x"
+ % (of.tell(), args.output, args.target_offset)
+ )
+
+
+def version(args):
+ from . import __version__
+
+ print(__version__)
diff --git a/installer/bin/esptool/esptool/config.py b/installer/bin/esptool/esptool/config.py
new file mode 100644
index 0000000..5566bec
--- /dev/null
+++ b/installer/bin/esptool/esptool/config.py
@@ -0,0 +1,92 @@
+# SPDX-FileCopyrightText: 2014-2023 Espressif Systems (Shanghai) CO LTD,
+# other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import configparser
+import os
+
+CONFIG_OPTIONS = [
+ "timeout",
+ "chip_erase_timeout",
+ "max_timeout",
+ "sync_timeout",
+ "md5_timeout_per_mb",
+ "erase_region_timeout_per_mb",
+ "erase_write_timeout_per_mb",
+ "mem_end_rom_timeout",
+ "serial_write_timeout",
+ "connect_attempts",
+ "write_block_attempts",
+ "reset_delay",
+ "custom_reset_sequence",
+]
+
+
+def _validate_config_file(file_path, verbose=False):
+ if not os.path.exists(file_path):
+ return False
+
+ cfg = configparser.RawConfigParser()
+ try:
+ cfg.read(file_path, encoding="UTF-8")
+ # Only consider it a valid config file if it contains [esptool] section
+ if cfg.has_section("esptool"):
+ if verbose:
+ unknown_opts = list(set(cfg.options("esptool")) - set(CONFIG_OPTIONS))
+ unknown_opts.sort()
+ no_of_unknown_opts = len(unknown_opts)
+ if no_of_unknown_opts > 0:
+ suffix = "s" if no_of_unknown_opts > 1 else ""
+ print(
+ "Ignoring unknown config file option{}: {}".format(
+ suffix, ", ".join(unknown_opts)
+ )
+ )
+ return True
+ except (UnicodeDecodeError, configparser.Error) as e:
+ if verbose:
+ print(f"Ignoring invalid config file {file_path}: {e}")
+ return False
+
+
+def _find_config_file(dir_path, verbose=False):
+ for candidate in ("esptool.cfg", "setup.cfg", "tox.ini"):
+ cfg_path = os.path.join(dir_path, candidate)
+ if _validate_config_file(cfg_path, verbose):
+ return cfg_path
+ return None
+
+
+def load_config_file(verbose=False):
+ set_with_env_var = False
+ env_var_path = os.environ.get("ESPTOOL_CFGFILE")
+ if env_var_path is not None and _validate_config_file(env_var_path):
+ cfg_file_path = env_var_path
+ set_with_env_var = True
+ else:
+ home_dir = os.path.expanduser("~")
+ os_config_dir = (
+ f"{home_dir}/.config/esptool"
+ if os.name == "posix"
+ else f"{home_dir}/AppData/Local/esptool/"
+ )
+ # Search priority: 1) current dir, 2) OS specific config dir, 3) home dir
+ for dir_path in (os.getcwd(), os_config_dir, home_dir):
+ cfg_file_path = _find_config_file(dir_path, verbose)
+ if cfg_file_path:
+ break
+
+ cfg = configparser.ConfigParser()
+ cfg["esptool"] = {} # Create an empty esptool config for when no file is found
+
+ if cfg_file_path is not None:
+ # If config file is found and validated, read and parse it
+ cfg.read(cfg_file_path)
+ if verbose:
+ msg = " (set with ESPTOOL_CFGFILE)" if set_with_env_var else ""
+ print(
+ f"Loaded custom configuration from "
+ f"{os.path.abspath(cfg_file_path)}{msg}"
+ )
+ return cfg, cfg_file_path
diff --git a/installer/bin/esptool/esptool/loader.py b/installer/bin/esptool/esptool/loader.py
new file mode 100644
index 0000000..eb64023
--- /dev/null
+++ b/installer/bin/esptool/esptool/loader.py
@@ -0,0 +1,1608 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import base64
+import hashlib
+import itertools
+import json
+import os
+import re
+import string
+import struct
+import sys
+import time
+
+from .config import load_config_file
+from .reset import (
+ ClassicReset,
+ CustomReset,
+ DEFAULT_RESET_DELAY,
+ HardReset,
+ USBJTAGSerialReset,
+ UnixTightReset,
+)
+from .util import FatalError, NotImplementedInROMError, UnsupportedCommandError
+from .util import byte, hexify, mask_to_shift, pad_to, strip_chip_name
+
+try:
+ import serial
+except ImportError:
+ print(
+ "Pyserial is not installed for %s. "
+ "Check the README for installation instructions." % (sys.executable)
+ )
+ raise
+
+# check 'serial' is 'pyserial' and not 'serial'
+# ref. https://github.com/espressif/esptool/issues/269
+try:
+ if "serialization" in serial.__doc__ and "deserialization" in serial.__doc__:
+ raise ImportError(
+ "esptool.py depends on pyserial, but there is a conflict with a currently "
+ "installed package named 'serial'.\n"
+ "You may work around this by 'pip uninstall serial; pip install pyserial' "
+ "but this may break other installed Python software "
+ "that depends on 'serial'.\n"
+ "There is no good fix for this right now, "
+ "apart from configuring virtualenvs. "
+ "See https://github.com/espressif/esptool/issues/269#issuecomment-385298196"
+ " for discussion of the underlying issue(s)."
+ )
+except TypeError:
+ pass # __doc__ returns None for pyserial
+
+try:
+ import serial.tools.list_ports as list_ports
+except ImportError:
+ print(
+ "The installed version (%s) of pyserial appears to be too old for esptool.py "
+ "(Python interpreter %s). Check the README for installation instructions."
+ % (sys.VERSION, sys.executable)
+ )
+ raise
+except Exception:
+ if sys.platform == "darwin":
+ # swallow the exception, this is a known issue in pyserial+macOS Big Sur preview
+ # ref https://github.com/espressif/esptool/issues/540
+ list_ports = None
+ else:
+ raise
+
+
+cfg, _ = load_config_file()
+cfg = cfg["esptool"]
+
+# Timeout for most flash operations
+DEFAULT_TIMEOUT = cfg.getfloat("timeout", 3)
+# Timeout for full chip erase
+CHIP_ERASE_TIMEOUT = cfg.getfloat("chip_erase_timeout", 120)
+# Longest any command can run
+MAX_TIMEOUT = cfg.getfloat("max_timeout", CHIP_ERASE_TIMEOUT * 2)
+# Timeout for syncing with bootloader
+SYNC_TIMEOUT = cfg.getfloat("sync_timeout", 0.1)
+# Timeout (per megabyte) for calculating md5sum
+MD5_TIMEOUT_PER_MB = cfg.getfloat("md5_timeout_per_mb", 8)
+# Timeout (per megabyte) for erasing a region
+ERASE_REGION_TIMEOUT_PER_MB = cfg.getfloat("erase_region_timeout_per_mb", 30)
+# Timeout (per megabyte) for erasing and writing data
+ERASE_WRITE_TIMEOUT_PER_MB = cfg.getfloat("erase_write_timeout_per_mb", 40)
+# Short timeout for ESP_MEM_END, as it may never respond
+MEM_END_ROM_TIMEOUT = cfg.getfloat("mem_end_rom_timeout", 0.2)
+# Timeout for serial port write
+DEFAULT_SERIAL_WRITE_TIMEOUT = cfg.getfloat("serial_write_timeout", 10)
+# Default number of times to try connection
+DEFAULT_CONNECT_ATTEMPTS = cfg.getint("connect_attempts", 7)
+# Number of times to try writing a data block
+WRITE_BLOCK_ATTEMPTS = cfg.getint("write_block_attempts", 3)
+
+STUBS_DIR = os.path.join(os.path.dirname(__file__), "./targets/stub_flasher/")
+
+
+def get_stub_json_path(chip_name):
+ chip_name = strip_chip_name(chip_name)
+ chip_name = chip_name.replace("esp", "")
+ return STUBS_DIR + "stub_flasher_" + chip_name + ".json"
+
+
+def timeout_per_mb(seconds_per_mb, size_bytes):
+ """Scales timeouts which are size-specific"""
+ result = seconds_per_mb * (size_bytes / 1e6)
+ if result < DEFAULT_TIMEOUT:
+ return DEFAULT_TIMEOUT
+ return result
+
+
+def check_supported_function(func, check_func):
+ """
+ Decorator implementation that wraps a check around an ESPLoader
+ bootloader function to check if it's supported.
+
+ This is used to capture the multidimensional differences in
+ functionality between the ESP8266 & ESP32 (and later chips) ROM loaders, and the
+ software stub that runs on these. Not possible to do this cleanly
+ via inheritance alone.
+ """
+
+ def inner(*args, **kwargs):
+ obj = args[0]
+ if check_func(obj):
+ return func(*args, **kwargs)
+ else:
+ raise NotImplementedInROMError(obj, func)
+
+ return inner
+
+
+def stub_function_only(func):
+ """Attribute for a function only supported in the software stub loader"""
+ return check_supported_function(func, lambda o: o.IS_STUB)
+
+
+def stub_and_esp32_function_only(func):
+ """Attribute for a function only supported by stubs or ESP32 and later chips ROM"""
+ return check_supported_function(
+ func, lambda o: o.IS_STUB or o.CHIP_NAME not in ["ESP8266"]
+ )
+
+
+def esp32s3_or_newer_function_only(func):
+ """Attribute for a function only supported by ESP32S3 and later chips ROM"""
+ return check_supported_function(
+ func, lambda o: o.CHIP_NAME not in ["ESP8266", "ESP32", "ESP32-S2"]
+ )
+
+
+class StubFlasher:
+ def __init__(self, json_path):
+ with open(json_path) as json_file:
+ stub = json.load(json_file)
+
+ self.text = base64.b64decode(stub["text"])
+ self.text_start = stub["text_start"]
+ self.entry = stub["entry"]
+
+ try:
+ self.data = base64.b64decode(stub["data"])
+ self.data_start = stub["data_start"]
+ except KeyError:
+ self.data = None
+ self.data_start = None
+
+
+class ESPLoader(object):
+ """Base class providing access to ESP ROM & software stub bootloaders.
+ Subclasses provide ESP8266 & ESP32 Family specific functionality.
+
+ Don't instantiate this base class directly, either instantiate a subclass or
+ call cmds.detect_chip() which will interrogate the chip and return the
+ appropriate subclass instance.
+
+ """
+
+ CHIP_NAME = "Espressif device"
+ IS_STUB = False
+
+ FPGA_SLOW_BOOT = False
+
+ DEFAULT_PORT = "/dev/ttyUSB0"
+
+ USES_RFC2217 = False
+
+ # Commands supported by ESP8266 ROM bootloader
+ ESP_FLASH_BEGIN = 0x02
+ ESP_FLASH_DATA = 0x03
+ ESP_FLASH_END = 0x04
+ ESP_MEM_BEGIN = 0x05
+ ESP_MEM_END = 0x06
+ ESP_MEM_DATA = 0x07
+ ESP_SYNC = 0x08
+ ESP_WRITE_REG = 0x09
+ ESP_READ_REG = 0x0A
+
+ # Some comands supported by ESP32 and later chips ROM bootloader (or -8266 w/ stub)
+ ESP_SPI_SET_PARAMS = 0x0B
+ ESP_SPI_ATTACH = 0x0D
+ ESP_READ_FLASH_SLOW = 0x0E # ROM only, much slower than the stub flash read
+ ESP_CHANGE_BAUDRATE = 0x0F
+ ESP_FLASH_DEFL_BEGIN = 0x10
+ ESP_FLASH_DEFL_DATA = 0x11
+ ESP_FLASH_DEFL_END = 0x12
+ ESP_SPI_FLASH_MD5 = 0x13
+
+ # Commands supported by ESP32-S2 and later chips ROM bootloader only
+ ESP_GET_SECURITY_INFO = 0x14
+
+ # Some commands supported by stub only
+ ESP_ERASE_FLASH = 0xD0
+ ESP_ERASE_REGION = 0xD1
+ ESP_READ_FLASH = 0xD2
+ ESP_RUN_USER_CODE = 0xD3
+
+ # Flash encryption encrypted data command
+ ESP_FLASH_ENCRYPT_DATA = 0xD4
+
+ # Response code(s) sent by ROM
+ ROM_INVALID_RECV_MSG = 0x05 # response if an invalid message is received
+
+ # Maximum block sized for RAM and Flash writes, respectively.
+ ESP_RAM_BLOCK = 0x1800
+
+ FLASH_WRITE_SIZE = 0x400
+
+ # Default baudrate. The ROM auto-bauds, so we can use more or less whatever we want.
+ ESP_ROM_BAUD = 115200
+
+ # First byte of the application image
+ ESP_IMAGE_MAGIC = 0xE9
+
+ # Initial state for the checksum routine
+ ESP_CHECKSUM_MAGIC = 0xEF
+
+ # Flash sector size, minimum unit of erase.
+ FLASH_SECTOR_SIZE = 0x1000
+
+ UART_DATE_REG_ADDR = 0x60000078
+
+ # This ROM address has a different value on each chip model
+ CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000
+
+ UART_CLKDIV_MASK = 0xFFFFF
+
+ # Memory addresses
+ IROM_MAP_START = 0x40200000
+ IROM_MAP_END = 0x40300000
+
+ # The number of bytes in the UART response that signify command status
+ STATUS_BYTES_LENGTH = 2
+
+ # Bootloader flashing offset
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ # ROM supports an encrypted flashing mode
+ SUPPORTS_ENCRYPTED_FLASH = False
+
+ # Response to ESP_SYNC might indicate that flasher stub is running
+ # instead of the ROM bootloader
+ sync_stub_detected = False
+
+ # Device PIDs
+ USB_JTAG_SERIAL_PID = 0x1001
+
+ # Chip IDs that are no longer supported by esptool
+ UNSUPPORTED_CHIPS = {6: "ESP32-S3(beta 3)"}
+
+ def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False):
+ """Base constructor for ESPLoader bootloader interaction
+
+ Don't call this constructor, either instantiate a specific
+ ROM class directly, or use cmds.detect_chip().
+
+ This base class has all of the instance methods for bootloader
+ functionality supported across various chips & stub
+ loaders. Subclasses replace the functions they don't support
+ with ones which throw NotImplementedInROMError().
+
+ """
+ # True if esptool detects the ROM is in Secure Download Mode
+ self.secure_download_mode = False
+ # True if esptool detects conditions which require the stub to be disabled
+ self.stub_is_disabled = False
+
+ # Device-and-runtime-specific cache
+ self.cache = {
+ "flash_id": None,
+ "chip_id": None,
+ "uart_no": None,
+ }
+
+ if isinstance(port, str):
+ try:
+ self._port = serial.serial_for_url(port)
+ except serial.serialutil.SerialException:
+ raise FatalError(f"Could not open {port}, the port doesn't exist")
+ else:
+ self._port = port
+ self._slip_reader = slip_reader(self._port, self.trace)
+ # setting baud rate in a separate step is a workaround for
+ # CH341 driver on some Linux versions (this opens at 9600 then
+ # sets), shouldn't matter for other platforms/drivers. See
+ # https://github.com/espressif/esptool/issues/44#issuecomment-107094446
+ self._set_port_baudrate(baud)
+ self._trace_enabled = trace_enabled
+ # set write timeout, to prevent esptool blocked at write forever.
+ try:
+ self._port.write_timeout = DEFAULT_SERIAL_WRITE_TIMEOUT
+ except NotImplementedError:
+ # no write timeout for RFC2217 ports
+ # need to set the property back to None or it will continue to fail
+ self._port.write_timeout = None
+
+ @property
+ def serial_port(self):
+ return self._port.port
+
+ def _set_port_baudrate(self, baud):
+ try:
+ self._port.baudrate = baud
+ except IOError:
+ raise FatalError(
+ "Failed to set baud rate %d. The driver may not support this rate."
+ % baud
+ )
+
+ def read(self):
+ """Read a SLIP packet from the serial port"""
+ return next(self._slip_reader)
+
+ def write(self, packet):
+ """Write bytes to the serial port while performing SLIP escaping"""
+ buf = (
+ b"\xc0"
+ + (packet.replace(b"\xdb", b"\xdb\xdd").replace(b"\xc0", b"\xdb\xdc"))
+ + b"\xc0"
+ )
+ self.trace("Write %d bytes: %s", len(buf), HexFormatter(buf))
+ self._port.write(buf)
+
+ def trace(self, message, *format_args):
+ if self._trace_enabled:
+ now = time.time()
+ try:
+ delta = now - self._last_trace
+ except AttributeError:
+ delta = 0.0
+ self._last_trace = now
+ prefix = "TRACE +%.3f " % delta
+ print(prefix + (message % format_args))
+
+ @staticmethod
+ def checksum(data, state=ESP_CHECKSUM_MAGIC):
+ """Calculate checksum of a blob, as it is defined by the ROM"""
+ for b in data:
+ state ^= b
+
+ return state
+
+ def command(
+ self,
+ op=None,
+ data=b"",
+ chk=0,
+ wait_response=True,
+ timeout=DEFAULT_TIMEOUT,
+ ):
+ """Send a request and read the response"""
+ saved_timeout = self._port.timeout
+ new_timeout = min(timeout, MAX_TIMEOUT)
+ if new_timeout != saved_timeout:
+ self._port.timeout = new_timeout
+
+ try:
+ if op is not None:
+ self.trace(
+ "command op=0x%02x data len=%s wait_response=%d "
+ "timeout=%.3f data=%s",
+ op,
+ len(data),
+ 1 if wait_response else 0,
+ timeout,
+ HexFormatter(data),
+ )
+ pkt = struct.pack(b" self.STATUS_BYTES_LENGTH:
+ return data[: -self.STATUS_BYTES_LENGTH]
+ else:
+ # otherwise, just return the 'val' field which comes from the reply header
+ # (this is used by read_reg)
+ return val
+
+ def flush_input(self):
+ self._port.flushInput()
+ self._slip_reader = slip_reader(self._port, self.trace)
+
+ def sync(self):
+ val, _ = self.command(
+ self.ESP_SYNC, b"\x07\x07\x12\x20" + 32 * b"\x55", timeout=SYNC_TIMEOUT
+ )
+
+ # ROM bootloaders send some non-zero "val" response. The flasher stub sends 0.
+ # If we receive 0 then it probably indicates that the chip wasn't or couldn't be
+ # reseted properly and esptool is talking to the flasher stub.
+ self.sync_stub_detected = val == 0
+
+ for _ in range(7):
+ val, _ = self.command()
+ self.sync_stub_detected &= val == 0
+
+ def _get_pid(self):
+ if list_ports is None:
+ print(
+ "\nListing all serial ports is currently not available. "
+ "Can't get device PID."
+ )
+ return
+ active_port = self._port.port
+
+ # Pyserial only identifies regular ports, URL handlers are not supported
+ if not active_port.lower().startswith(("com", "/dev/")):
+ print(
+ "\nDevice PID identification is only supported on "
+ "COM and /dev/ serial ports."
+ )
+ return
+ # Return the real path if the active port is a symlink
+ if active_port.startswith("/dev/") and os.path.islink(active_port):
+ active_port = os.path.realpath(active_port)
+
+ # The "cu" (call-up) device has to be used for outgoing communication on MacOS
+ if sys.platform == "darwin" and "tty" in active_port:
+ active_port = [active_port, active_port.replace("tty", "cu")]
+ ports = list_ports.comports()
+ for p in ports:
+ if p.device in active_port:
+ return p.pid
+ print(
+ "\nFailed to get PID of a device on {}, "
+ "using standard reset sequence.".format(active_port)
+ )
+
+ def _connect_attempt(self, reset_strategy, mode="default_reset"):
+ """A single connection attempt"""
+ last_error = None
+ boot_log_detected = False
+ download_mode = False
+
+ # If we're doing no_sync, we're likely communicating as a pass through
+ # with an intermediate device to the ESP32
+ if mode == "no_reset_no_sync":
+ return last_error
+
+ if mode != "no_reset":
+ if not self.USES_RFC2217: # Might block on rfc2217 ports
+ # Empty serial buffer to isolate boot log
+ self._port.reset_input_buffer()
+
+ reset_strategy() # Reset the chip to bootloader (download mode)
+
+ # Detect the ROM boot log and check actual boot mode (ESP32 and later only)
+ waiting = self._port.inWaiting()
+ read_bytes = self._port.read(waiting)
+ data = re.search(
+ b"boot:(0x[0-9a-fA-F]+)(.*waiting for download)?", read_bytes, re.DOTALL
+ )
+ if data is not None:
+ boot_log_detected = True
+ boot_mode = data.group(1)
+ download_mode = data.group(2) is not None
+
+ for _ in range(5):
+ try:
+ self.flush_input()
+ self._port.flushOutput()
+ self.sync()
+ return None
+ except FatalError as e:
+ print(".", end="")
+ sys.stdout.flush()
+ time.sleep(0.05)
+ last_error = e
+
+ if boot_log_detected:
+ last_error = FatalError(
+ "Wrong boot mode detected ({})! "
+ "The chip needs to be in download mode.".format(
+ boot_mode.decode("utf-8")
+ )
+ )
+ if download_mode:
+ last_error = FatalError(
+ "Download mode successfully detected, but getting no sync reply: "
+ "The serial TX path seems to be down."
+ )
+ return last_error
+
+ def get_memory_region(self, name):
+ """
+ Returns a tuple of (start, end) for the memory map entry with the given name,
+ or None if it doesn't exist
+ """
+ try:
+ return [(start, end) for (start, end, n) in self.MEMORY_MAP if n == name][0]
+ except IndexError:
+ return None
+
+ def _construct_reset_strategy_sequence(self, mode):
+ """
+ Constructs a sequence of reset strategies based on the OS,
+ used ESP chip, external settings, and environment variables.
+ Returns a tuple of one or more reset strategies to be tried sequentially.
+ """
+ cfg_custom_reset_sequence = cfg.get("custom_reset_sequence")
+ if cfg_custom_reset_sequence is not None:
+ return (CustomReset(self._port, cfg_custom_reset_sequence),)
+
+ cfg_reset_delay = cfg.getfloat("reset_delay")
+ if cfg_reset_delay is not None:
+ delay = extra_delay = cfg_reset_delay
+ else:
+ delay = DEFAULT_RESET_DELAY
+ extra_delay = DEFAULT_RESET_DELAY + 0.5
+
+ # This FPGA delay is for Espressif internal use
+ if (
+ self.FPGA_SLOW_BOOT
+ and os.environ.get("ESPTOOL_ENV_FPGA", "").strip() == "1"
+ ):
+ delay = extra_delay = 7
+
+ # USB-JTAG/Serial mode
+ if mode == "usb_reset" or self._get_pid() == self.USB_JTAG_SERIAL_PID:
+ return (USBJTAGSerialReset(self._port),)
+
+ # USB-to-Serial bridge
+ if os.name != "nt" and not self._port.name.startswith("rfc2217:"):
+ return (
+ UnixTightReset(self._port, delay),
+ UnixTightReset(self._port, extra_delay),
+ ClassicReset(self._port, delay),
+ ClassicReset(self._port, extra_delay),
+ )
+
+ return (
+ ClassicReset(self._port, delay),
+ ClassicReset(self._port, extra_delay),
+ )
+
+ def connect(
+ self,
+ mode="default_reset",
+ attempts=DEFAULT_CONNECT_ATTEMPTS,
+ detecting=False,
+ warnings=True,
+ ):
+ """Try connecting repeatedly until successful, or giving up"""
+ if warnings and mode in ["no_reset", "no_reset_no_sync"]:
+ print(
+ 'WARNING: Pre-connection option "{}" was selected.'.format(mode),
+ "Connection may fail if the chip is not in bootloader "
+ "or flasher stub mode.",
+ )
+ print("Connecting...", end="")
+ sys.stdout.flush()
+ last_error = None
+
+ reset_sequence = self._construct_reset_strategy_sequence(mode)
+ try:
+ for _, reset_strategy in zip(
+ range(attempts) if attempts > 0 else itertools.count(),
+ itertools.cycle(reset_sequence),
+ ):
+ last_error = self._connect_attempt(reset_strategy, mode)
+ if last_error is None:
+ break
+ finally:
+ print("") # end 'Connecting...' line
+
+ if last_error is not None:
+ raise FatalError(
+ "Failed to connect to {}: {}"
+ "\nFor troubleshooting steps visit: "
+ "https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html".format( # noqa E501
+ self.CHIP_NAME, last_error
+ )
+ )
+
+ if not detecting:
+ try:
+ from .targets import ROM_LIST
+
+ # check the date code registers match what we expect to see
+ chip_magic_value = self.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR)
+ if chip_magic_value not in self.CHIP_DETECT_MAGIC_VALUE:
+ actually = None
+ for cls in ROM_LIST:
+ if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE:
+ actually = cls
+ break
+ if warnings and actually is None:
+ print(
+ "WARNING: This chip doesn't appear to be a %s "
+ "(chip magic value 0x%08x). "
+ "Probably it is unsupported by this version of esptool."
+ % (self.CHIP_NAME, chip_magic_value)
+ )
+ else:
+ raise FatalError(
+ "This chip is %s not %s. Wrong --chip argument?"
+ % (actually.CHIP_NAME, self.CHIP_NAME)
+ )
+ except UnsupportedCommandError:
+ self.secure_download_mode = True
+
+ try:
+ self.check_chip_id()
+ except UnsupportedCommandError:
+ # Fix for ROM not responding in SDM, reconnect and try again
+ if self.secure_download_mode:
+ self._connect_attempt(mode, reset_sequence[0])
+ self.check_chip_id()
+ else:
+ raise
+ self._post_connect()
+
+ def _post_connect(self):
+ """
+ Additional initialization hook, may be overridden by the chip-specific class.
+ Gets called after connect, and after auto-detection.
+ """
+ pass
+
+ def read_reg(self, addr, timeout=DEFAULT_TIMEOUT):
+ """Read memory address in target"""
+ # we don't call check_command here because read_reg() function is called
+ # when detecting chip type, and the way we check for success
+ # (STATUS_BYTES_LENGTH) is different for different chip types (!)
+ val, data = self.command(
+ self.ESP_READ_REG, struct.pack(" 0:
+ # add a dummy write to a date register as an excuse to have a delay
+ command += struct.pack(
+ " start:
+ raise FatalError(
+ "Software loader is resident at 0x%08x-0x%08x. "
+ "Can't load binary at overlapping address range 0x%08x-0x%08x. "
+ "Either change binary loading address, or use the --no-stub "
+ "option to disable the software loader."
+ % (start, end, load_start, load_end)
+ )
+
+ return self.check_command(
+ "enter RAM download mode",
+ self.ESP_MEM_BEGIN,
+ struct.pack(" length:
+ raise FatalError("Read more than expected")
+
+ digest_frame = self.read()
+ if len(digest_frame) != 16:
+ raise FatalError("Expected digest, got: %s" % hexify(digest_frame))
+ expected_digest = hexify(digest_frame).upper()
+ digest = hashlib.md5(data).hexdigest().upper()
+ if digest != expected_digest:
+ raise FatalError(
+ "Digest mismatch: expected %s, got %s" % (expected_digest, digest)
+ )
+ return data
+
+ def flash_spi_attach(self, hspi_arg):
+ """Send SPI attach command to enable the SPI flash pins
+
+ ESP8266 ROM does this when you send flash_begin, ESP32 ROM
+ has it as a SPI command.
+ """
+ # last 3 bytes in ESP_SPI_ATTACH argument are reserved values
+ arg = struct.pack(" 0:
+ self.write_reg(SPI_MOSI_DLEN_REG, mosi_bits - 1)
+ if miso_bits > 0:
+ self.write_reg(SPI_MISO_DLEN_REG, miso_bits - 1)
+ flags = 0
+ if dummy_len > 0:
+ flags |= dummy_len - 1
+ if addr_len > 0:
+ flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT
+ if flags:
+ self.write_reg(SPI_USR1_REG, flags)
+
+ else:
+
+ def set_data_lengths(mosi_bits, miso_bits):
+ SPI_DATA_LEN_REG = SPI_USR1_REG
+ SPI_MOSI_BITLEN_S = 17
+ SPI_MISO_BITLEN_S = 8
+ mosi_mask = 0 if (mosi_bits == 0) else (mosi_bits - 1)
+ miso_mask = 0 if (miso_bits == 0) else (miso_bits - 1)
+ flags = (miso_mask << SPI_MISO_BITLEN_S) | (
+ mosi_mask << SPI_MOSI_BITLEN_S
+ )
+ if dummy_len > 0:
+ flags |= dummy_len - 1
+ if addr_len > 0:
+ flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT
+ self.write_reg(SPI_DATA_LEN_REG, flags)
+
+ # SPI peripheral "command" bitmasks for SPI_CMD_REG
+ SPI_CMD_USR = 1 << 18
+
+ # shift values
+ SPI_USR2_COMMAND_LEN_SHIFT = 28
+ SPI_USR_ADDR_LEN_SHIFT = 26
+
+ if read_bits > 32:
+ raise FatalError(
+ "Reading more than 32 bits back from a SPI flash "
+ "operation is unsupported"
+ )
+ if len(data) > 64:
+ raise FatalError(
+ "Writing more than 64 bytes of data with one SPI "
+ "command is unsupported"
+ )
+
+ data_bits = len(data) * 8
+ old_spi_usr = self.read_reg(SPI_USR_REG)
+ old_spi_usr2 = self.read_reg(SPI_USR2_REG)
+ flags = SPI_USR_COMMAND
+ if read_bits > 0:
+ flags |= SPI_USR_MISO
+ if data_bits > 0:
+ flags |= SPI_USR_MOSI
+ if addr_len > 0:
+ flags |= SPI_USR_ADDR
+ if dummy_len > 0:
+ flags |= SPI_USR_DUMMY
+ set_data_lengths(data_bits, read_bits)
+ self.write_reg(SPI_USR_REG, flags)
+ self.write_reg(
+ SPI_USR2_REG, (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflash_command
+ )
+ if addr and addr_len > 0:
+ self.write_reg(SPI_ADDR_REG, addr)
+ if data_bits == 0:
+ self.write_reg(SPI_W0_REG, 0) # clear data register before we read it
+ else:
+ data = pad_to(data, 4, b"\00") # pad to 32-bit multiple
+ words = struct.unpack("I" * (len(data) // 4), data)
+ next_reg = SPI_W0_REG
+ for word in words:
+ self.write_reg(next_reg, word)
+ next_reg += 4
+ self.write_reg(SPI_CMD_REG, SPI_CMD_USR)
+
+ def wait_done():
+ for _ in range(10):
+ if (self.read_reg(SPI_CMD_REG) & SPI_CMD_USR) == 0:
+ return
+ raise FatalError("SPI command did not complete in time")
+
+ wait_done()
+
+ status = self.read_reg(SPI_W0_REG)
+ # restore some SPI controller registers
+ self.write_reg(SPI_USR_REG, old_spi_usr)
+ self.write_reg(SPI_USR2_REG, old_spi_usr2)
+ return status
+
+ def read_spiflash_sfdp(self, addr, read_bits):
+ CMD_RDSFDP = 0x5A
+ return self.run_spiflash_command(
+ CMD_RDSFDP, read_bits=read_bits, addr=addr, addr_len=24, dummy_len=8
+ )
+
+ def read_status(self, num_bytes=2):
+ """Read up to 24 bits (num_bytes) of SPI flash status register contents
+ via RDSR, RDSR2, RDSR3 commands
+
+ Not all SPI flash supports all three commands. The upper 1 or 2
+ bytes may be 0xFF.
+ """
+ SPIFLASH_RDSR = 0x05
+ SPIFLASH_RDSR2 = 0x35
+ SPIFLASH_RDSR3 = 0x15
+
+ status = 0
+ shift = 0
+ for cmd in [SPIFLASH_RDSR, SPIFLASH_RDSR2, SPIFLASH_RDSR3][0:num_bytes]:
+ status += self.run_spiflash_command(cmd, read_bits=8) << shift
+ shift += 8
+ return status
+
+ def write_status(self, new_status, num_bytes=2, set_non_volatile=False):
+ """Write up to 24 bits (num_bytes) of new status register
+
+ num_bytes can be 1, 2 or 3.
+
+ Not all flash supports the additional commands to write the
+ second and third byte of the status register. When writing 2
+ bytes, esptool also sends a 16-byte WRSR command (as some
+ flash types use this instead of WRSR2.)
+
+ If the set_non_volatile flag is set, non-volatile bits will
+ be set as well as volatile ones (WREN used instead of WEVSR).
+
+ """
+ SPIFLASH_WRSR = 0x01
+ SPIFLASH_WRSR2 = 0x31
+ SPIFLASH_WRSR3 = 0x11
+ SPIFLASH_WEVSR = 0x50
+ SPIFLASH_WREN = 0x06
+ SPIFLASH_WRDI = 0x04
+
+ enable_cmd = SPIFLASH_WREN if set_non_volatile else SPIFLASH_WEVSR
+
+ # try using a 16-bit WRSR (not supported by all chips)
+ # this may be redundant, but shouldn't hurt
+ if num_bytes == 2:
+ self.run_spiflash_command(enable_cmd)
+ self.run_spiflash_command(SPIFLASH_WRSR, struct.pack(">= 8
+
+ self.run_spiflash_command(SPIFLASH_WRDI)
+
+ def get_crystal_freq(self):
+ """
+ Figure out the crystal frequency from the UART clock divider
+
+ Returns a normalized value in integer MHz (only values 40 or 26 are supported)
+ """
+ # The logic here is:
+ # - We know that our baud rate and the ESP UART baud rate are roughly the same,
+ # or we couldn't communicate
+ # - We can read the UART clock divider register to know how the ESP derives this
+ # from the APB bus frequency
+ # - Multiplying these two together gives us the bus frequency which is either
+ # the crystal frequency (ESP32) or double the crystal frequency (ESP8266).
+ # See the self.XTAL_CLK_DIVIDER parameter for this factor.
+ uart_div = self.read_reg(self.UART_CLKDIV_REG) & self.UART_CLKDIV_MASK
+ est_xtal = (self._port.baudrate * uart_div) / 1e6 / self.XTAL_CLK_DIVIDER
+ norm_xtal = 40 if est_xtal > 33 else 26
+ if abs(norm_xtal - est_xtal) > 1:
+ print(
+ "WARNING: Detected crystal freq %.2fMHz is quite different to "
+ "normalized freq %dMHz. Unsupported crystal in use?"
+ % (est_xtal, norm_xtal)
+ )
+ return norm_xtal
+
+ def hard_reset(self):
+ print("Hard resetting via RTS pin...")
+ HardReset(self._port)()
+
+ def soft_reset(self, stay_in_bootloader):
+ if not self.IS_STUB:
+ if stay_in_bootloader:
+ return # ROM bootloader is already in bootloader!
+ else:
+ # 'run user code' is as close to a soft reset as we can do
+ self.flash_begin(0, 0)
+ self.flash_finish(False)
+ else:
+ if stay_in_bootloader:
+ # soft resetting from the stub loader
+ # will re-load the ROM bootloader
+ self.flash_begin(0, 0)
+ self.flash_finish(True)
+ elif self.CHIP_NAME != "ESP8266":
+ raise FatalError(
+ "Soft resetting is currently only supported on ESP8266"
+ )
+ else:
+ # running user code from stub loader requires some hacks
+ # in the stub loader
+ self.command(self.ESP_RUN_USER_CODE, wait_response=False)
+
+ def check_chip_id(self):
+ try:
+ chip_id = self.get_chip_id()
+ if chip_id != self.IMAGE_CHIP_ID:
+ print(
+ "WARNING: Chip ID {} ({}) doesn't match expected Chip ID {}. "
+ "esptool may not work correctly.".format(
+ chip_id,
+ self.UNSUPPORTED_CHIPS.get(chip_id, "Unknown"),
+ self.IMAGE_CHIP_ID,
+ )
+ )
+ # Try to flash anyways by disabling stub
+ self.stub_is_disabled = True
+ except NotImplementedInROMError:
+ pass
+
+
+def slip_reader(port, trace_function):
+ """Generator to read SLIP packets from a serial port.
+ Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
+
+ Designed to avoid too many calls to serial.read(1), which can bog
+ down on slow systems.
+ """
+
+ def detect_panic_handler(input):
+ """
+ Checks the input bytes for panic handler messages.
+ Raises a FatalError if Guru Meditation or Fatal Exception is found, as both
+ of these are used between different ROM versions.
+ Tries to also parse the error cause (e.g. IllegalInstruction).
+ """
+
+ guru_meditation = (
+ rb"G?uru Meditation Error: (?:Core \d panic'ed \(([a-zA-Z ]*)\))?"
+ )
+ fatal_exception = rb"F?atal exception \(\d+\): (?:([a-zA-Z ]*)?.*epc)?"
+
+ # Search either for Guru Meditation or Fatal Exception
+ data = re.search(
+ rb"".join([rb"(?:", guru_meditation, rb"|", fatal_exception, rb")"]),
+ input,
+ re.DOTALL,
+ )
+ if data is not None:
+ cause = [
+ "({})".format(i.decode("utf-8"))
+ for i in [data.group(1), data.group(2)]
+ if i is not None
+ ]
+ cause = f" {cause[0]}" if len(cause) else ""
+ msg = f"Guru Meditation Error detected{cause}"
+ raise FatalError(msg)
+
+ partial_packet = None
+ in_escape = False
+ successful_slip = False
+ while True:
+ waiting = port.inWaiting()
+ read_bytes = port.read(1 if waiting == 0 else waiting)
+ if read_bytes == b"":
+ if partial_packet is None: # fail due to no data
+ msg = (
+ "Serial data stream stopped: Possible serial noise or corruption."
+ if successful_slip
+ else "No serial data received."
+ )
+ else: # fail during packet transfer
+ msg = "Packet content transfer stopped (received {} bytes)".format(
+ len(partial_packet)
+ )
+ trace_function(msg)
+ raise FatalError(msg)
+ trace_function("Read %d bytes: %s", len(read_bytes), HexFormatter(read_bytes))
+ for b in read_bytes:
+ b = bytes([b])
+ if partial_packet is None: # waiting for packet header
+ if b == b"\xc0":
+ partial_packet = b""
+ else:
+ trace_function("Read invalid data: %s", HexFormatter(read_bytes))
+ remaining_data = port.read(port.inWaiting())
+ trace_function(
+ "Remaining data in serial buffer: %s",
+ HexFormatter(remaining_data),
+ )
+ detect_panic_handler(read_bytes + remaining_data)
+ raise FatalError(
+ "Invalid head of packet (0x%s): "
+ "Possible serial noise or corruption." % hexify(b)
+ )
+ elif in_escape: # part-way through escape sequence
+ in_escape = False
+ if b == b"\xdc":
+ partial_packet += b"\xc0"
+ elif b == b"\xdd":
+ partial_packet += b"\xdb"
+ else:
+ trace_function("Read invalid data: %s", HexFormatter(read_bytes))
+ remaining_data = port.read(port.inWaiting())
+ trace_function(
+ "Remaining data in serial buffer: %s",
+ HexFormatter(remaining_data),
+ )
+ detect_panic_handler(read_bytes + remaining_data)
+ raise FatalError("Invalid SLIP escape (0xdb, 0x%s)" % (hexify(b)))
+ elif b == b"\xdb": # start of escape sequence
+ in_escape = True
+ elif b == b"\xc0": # end of packet
+ trace_function("Received full packet: %s", HexFormatter(partial_packet))
+ yield partial_packet
+ partial_packet = None
+ successful_slip = True
+ else: # normal byte in packet
+ partial_packet += b
+
+
+class HexFormatter(object):
+ """
+ Wrapper class which takes binary data in its constructor
+ and returns a hex string as it's __str__ method.
+
+ This is intended for "lazy formatting" of trace() output
+ in hex format. Avoids overhead (significant on slow computers)
+ of generating long hex strings even if tracing is disabled.
+
+ Note that this doesn't save any overhead if passed as an
+ argument to "%", only when passed to trace()
+
+ If auto_split is set (default), any long line (> 16 bytes) will be
+ printed as separately indented lines, with ASCII decoding at the end
+ of each line.
+ """
+
+ def __init__(self, binary_string, auto_split=True):
+ self._s = binary_string
+ self._auto_split = auto_split
+
+ def __str__(self):
+ if self._auto_split and len(self._s) > 16:
+ result = ""
+ s = self._s
+ while len(s) > 0:
+ line = s[:16]
+ ascii_line = "".join(
+ c
+ if (
+ c == " "
+ or (c in string.printable and c not in string.whitespace)
+ )
+ else "."
+ for c in line.decode("ascii", "replace")
+ )
+ s = s[16:]
+ result += "\n %-16s %-16s | %s" % (
+ hexify(line[:8], False),
+ hexify(line[8:], False),
+ ascii_line,
+ )
+ return result
+ else:
+ return hexify(self._s, False)
diff --git a/installer/bin/esptool/esptool/reset.py b/installer/bin/esptool/esptool/reset.py
new file mode 100644
index 0000000..39b9a15
--- /dev/null
+++ b/installer/bin/esptool/esptool/reset.py
@@ -0,0 +1,178 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import struct
+import time
+
+from .util import FatalError
+
+# Used for resetting into bootloader on Unix-like systems
+if os.name != "nt":
+ import fcntl
+ import termios
+
+ # Constants used for terminal status lines reading/setting.
+ # Taken from pySerial's backend for IO:
+ # https://github.com/pyserial/pyserial/blob/master/serial/serialposix.py
+ TIOCMSET = getattr(termios, "TIOCMSET", 0x5418)
+ TIOCMGET = getattr(termios, "TIOCMGET", 0x5415)
+ TIOCM_DTR = getattr(termios, "TIOCM_DTR", 0x002)
+ TIOCM_RTS = getattr(termios, "TIOCM_RTS", 0x004)
+
+DEFAULT_RESET_DELAY = 0.05 # default time to wait before releasing boot pin after reset
+
+
+class ResetStrategy(object):
+ def __init__(self, port, reset_delay=DEFAULT_RESET_DELAY):
+ self.port = port
+ self.reset_delay = reset_delay
+
+ def __call__():
+ pass
+
+ def _setDTR(self, state):
+ self.port.setDTR(state)
+
+ def _setRTS(self, state):
+ self.port.setRTS(state)
+ # Work-around for adapters on Windows using the usbser.sys driver:
+ # generate a dummy change to DTR so that the set-control-line-state
+ # request is sent with the updated RTS state and the same DTR state
+ self.port.setDTR(self.port.dtr)
+
+ def _setDTRandRTS(self, dtr=False, rts=False):
+ status = struct.unpack(
+ "I", fcntl.ioctl(self.port.fileno(), TIOCMGET, struct.pack("I", 0))
+ )[0]
+ if dtr:
+ status |= TIOCM_DTR
+ else:
+ status &= ~TIOCM_DTR
+ if rts:
+ status |= TIOCM_RTS
+ else:
+ status &= ~TIOCM_RTS
+ fcntl.ioctl(self.port.fileno(), TIOCMSET, struct.pack("I", status))
+
+
+class ClassicReset(ResetStrategy):
+ """
+ Classic reset sequence, sets DTR and RTS lines sequentially.
+ """
+
+ def __call__(self):
+ self._setDTR(False) # IO0=HIGH
+ self._setRTS(True) # EN=LOW, chip in reset
+ time.sleep(0.1)
+ self._setDTR(True) # IO0=LOW
+ self._setRTS(False) # EN=HIGH, chip out of reset
+ time.sleep(self.reset_delay)
+ self._setDTR(False) # IO0=HIGH, done
+
+
+class UnixTightReset(ResetStrategy):
+ """
+ UNIX-only reset sequence with custom implementation,
+ which allows setting DTR and RTS lines at the same time.
+ """
+
+ def __call__(self):
+ self._setDTRandRTS(False, False)
+ self._setDTRandRTS(True, True)
+ self._setDTRandRTS(False, True) # IO0=HIGH & EN=LOW, chip in reset
+ time.sleep(0.1)
+ self._setDTRandRTS(True, False) # IO0=LOW & EN=HIGH, chip out of reset
+ time.sleep(self.reset_delay)
+ self._setDTRandRTS(False, False) # IO0=HIGH, done
+ self._setDTR(False) # Needed in some environments to ensure IO0=HIGH
+
+
+class USBJTAGSerialReset(ResetStrategy):
+ """
+ Custom reset sequence, which is required when the device
+ is connecting via its USB-JTAG-Serial peripheral.
+ """
+
+ def __call__(self):
+ self._setRTS(False)
+ self._setDTR(False) # Idle
+ time.sleep(0.1)
+ self._setDTR(True) # Set IO0
+ self._setRTS(False)
+ time.sleep(0.1)
+ self._setRTS(True) # Reset. Calls inverted to go through (1,1) instead of (0,0)
+ self._setDTR(False)
+ self._setRTS(True) # RTS set as Windows only propagates DTR on RTS setting
+ time.sleep(0.1)
+ self._setDTR(False)
+ self._setRTS(False) # Chip out of reset
+
+
+class HardReset(ResetStrategy):
+ """
+ Reset sequence for hard resetting the chip.
+ Can be used to reset out of the bootloader or to restart a running app.
+ """
+
+ def __init__(self, port, uses_usb_otg=False):
+ super().__init__(port)
+ self.uses_usb_otg = uses_usb_otg
+
+ def __call__(self):
+ self._setRTS(True) # EN->LOW
+ if self.uses_usb_otg:
+ # Give the chip some time to come out of reset,
+ # to be able to handle further DTR/RTS transitions
+ time.sleep(0.2)
+ self._setRTS(False)
+ time.sleep(0.2)
+ else:
+ time.sleep(0.1)
+ self._setRTS(False)
+
+
+class CustomReset(ResetStrategy):
+ """
+ Custom reset strategy defined with a string.
+
+ CustomReset object is created as "rst = CustomReset(port, seq_str)"
+ and can be later executed simply with "rst()"
+
+ The seq_str input string consists of individual commands divided by "|".
+ Commands (e.g. R0) are defined by a code (R) and an argument (0).
+
+ The commands are:
+ D: setDTR - 1=True / 0=False
+ R: setRTS - 1=True / 0=False
+ U: setDTRandRTS (Unix-only) - 0,0 / 0,1 / 1,0 / or 1,1
+ W: Wait (time delay) - positive float number
+
+ e.g.
+ "D0|R1|W0.1|D1|R0|W0.05|D0" represents the ClassicReset strategy
+ "U1,1|U0,1|W0.1|U1,0|W0.05|U0,0" represents the UnixTightReset strategy
+ """
+
+ format_dict = {
+ "D": "self.port.setDTR({})",
+ "R": "self.port.setRTS({})",
+ "W": "time.sleep({})",
+ "U": "self._setDTRandRTS({})",
+ }
+
+ def __call__(self):
+ exec(self.constructed_strategy)
+
+ def __init__(self, port, seq_str):
+ super().__init__(port)
+ self.constructed_strategy = self._parse_string_to_seq(seq_str)
+
+ def _parse_string_to_seq(self, seq_str):
+ try:
+ cmds = seq_str.split("|")
+ fn_calls_list = [self.format_dict[cmd[0]].format(cmd[1:]) for cmd in cmds]
+ except Exception as e:
+ raise FatalError(f'Invalid "custom_reset_sequence" option format: {e}')
+ return "\n".join(fn_calls_list)
diff --git a/installer/bin/esptool/esptool/targets/__init__.py b/installer/bin/esptool/esptool/targets/__init__.py
new file mode 100644
index 0000000..9d76ca5
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/__init__.py
@@ -0,0 +1,31 @@
+from .esp32 import ESP32ROM
+from .esp32c2 import ESP32C2ROM
+from .esp32c3 import ESP32C3ROM
+from .esp32c6 import ESP32C6ROM
+from .esp32c6beta import ESP32C6BETAROM
+from .esp32h2 import ESP32H2ROM
+from .esp32h2beta1 import ESP32H2BETA1ROM
+from .esp32h2beta2 import ESP32H2BETA2ROM
+from .esp32s2 import ESP32S2ROM
+from .esp32s3 import ESP32S3ROM
+from .esp32s3beta2 import ESP32S3BETA2ROM
+from .esp8266 import ESP8266ROM
+
+
+CHIP_DEFS = {
+ "esp8266": ESP8266ROM,
+ "esp32": ESP32ROM,
+ "esp32s2": ESP32S2ROM,
+ "esp32s3beta2": ESP32S3BETA2ROM,
+ "esp32s3": ESP32S3ROM,
+ "esp32c3": ESP32C3ROM,
+ "esp32c6beta": ESP32C6BETAROM,
+ "esp32h2beta1": ESP32H2BETA1ROM,
+ "esp32h2beta2": ESP32H2BETA2ROM,
+ "esp32c2": ESP32C2ROM,
+ "esp32c6": ESP32C6ROM,
+ "esp32h2": ESP32H2ROM,
+}
+
+CHIP_LIST = list(CHIP_DEFS.keys())
+ROM_LIST = list(CHIP_DEFS.values())
diff --git a/installer/bin/esptool/esptool/targets/esp32.py b/installer/bin/esptool/esptool/targets/esp32.py
new file mode 100644
index 0000000..b805ed4
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32.py
@@ -0,0 +1,393 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+import time
+
+from ..loader import ESPLoader
+from ..util import FatalError, NotSupportedError
+
+
+class ESP32ROM(ESPLoader):
+ """Access class for ESP32 ROM bootloader"""
+
+ CHIP_NAME = "ESP32"
+ IMAGE_CHIP_ID = 0
+ IS_STUB = False
+
+ FPGA_SLOW_BOOT = True
+
+ CHIP_DETECT_MAGIC_VALUE = [0x00F01D83]
+
+ IROM_MAP_START = 0x400D0000
+ IROM_MAP_END = 0x40400000
+
+ DROM_MAP_START = 0x3F400000
+ DROM_MAP_END = 0x3F800000
+
+ # ESP32 uses a 4 byte status reply
+ STATUS_BYTES_LENGTH = 4
+
+ SPI_REG_BASE = 0x3FF42000
+ SPI_USR_OFFS = 0x1C
+ SPI_USR1_OFFS = 0x20
+ SPI_USR2_OFFS = 0x24
+ SPI_MOSI_DLEN_OFFS = 0x28
+ SPI_MISO_DLEN_OFFS = 0x2C
+ EFUSE_RD_REG_BASE = 0x3FF5A000
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + 0x18
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 7 # EFUSE_RD_DISABLE_DL_ENCRYPT
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_RD_REG_BASE # EFUSE_BLK0_WDATA0_REG
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7F << 20 # EFUSE_FLASH_CRYPT_CNT
+
+ EFUSE_RD_ABS_DONE_REG = EFUSE_RD_REG_BASE + 0x018
+ EFUSE_RD_ABS_DONE_0_MASK = 1 << 4
+ EFUSE_RD_ABS_DONE_1_MASK = 1 << 5
+
+ DR_REG_SYSCON_BASE = 0x3FF66000
+ APB_CTL_DATE_ADDR = DR_REG_SYSCON_BASE + 0x7C
+ APB_CTL_DATE_V = 0x1
+ APB_CTL_DATE_S = 31
+
+ SPI_W0_OFFS = 0x80
+
+ UART_CLKDIV_REG = 0x3FF40014
+
+ XTAL_CLK_DIVIDER = 1
+
+ RTCCALICFG1 = 0x3FF5F06C
+ TIMERS_RTC_CALI_VALUE = 0x01FFFFFF
+ TIMERS_RTC_CALI_VALUE_S = 7
+
+ FLASH_SIZES = {
+ "1MB": 0x00,
+ "2MB": 0x10,
+ "4MB": 0x20,
+ "8MB": 0x30,
+ "16MB": 0x40,
+ "32MB": 0x50,
+ "64MB": 0x60,
+ "128MB": 0x70,
+ }
+
+ FLASH_FREQUENCY = {
+ "80m": 0xF,
+ "40m": 0x0,
+ "26m": 0x1,
+ "20m": 0x2,
+ }
+
+ BOOTLOADER_FLASH_OFFSET = 0x1000
+
+ OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"]
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x3F400000, 0x3F800000, "DROM"],
+ [0x3F800000, 0x3FC00000, "EXTRAM_DATA"],
+ [0x3FF80000, 0x3FF82000, "RTC_DRAM"],
+ [0x3FF90000, 0x40000000, "BYTE_ACCESSIBLE"],
+ [0x3FFAE000, 0x40000000, "DRAM"],
+ [0x3FFE0000, 0x3FFFFFFC, "DIRAM_DRAM"],
+ [0x40000000, 0x40070000, "IROM"],
+ [0x40070000, 0x40078000, "CACHE_PRO"],
+ [0x40078000, 0x40080000, "CACHE_APP"],
+ [0x40080000, 0x400A0000, "IRAM"],
+ [0x400A0000, 0x400BFFFC, "DIRAM_IRAM"],
+ [0x400C0000, 0x400C2000, "RTC_IRAM"],
+ [0x400D0000, 0x40400000, "IROM"],
+ [0x50000000, 0x50002000, "RTC_DATA"],
+ ]
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 32
+
+ """ Try to read the BLOCK1 (encryption key) and check if it is valid """
+
+ def is_flash_encryption_key_valid(self):
+ """Bit 0 of efuse_rd_disable[3:0] is mapped to BLOCK1
+ this bit is at position 16 in EFUSE_BLK0_RDATA0_REG"""
+ word0 = self.read_efuse(0)
+ rd_disable = (word0 >> 16) & 0x1
+
+ # reading of BLOCK1 is NOT ALLOWED so we assume valid key is programmed
+ if rd_disable:
+ return True
+ else:
+ # reading of BLOCK1 is ALLOWED so we will read and verify for non-zero.
+ # When ESP32 has not generated AES/encryption key in BLOCK1,
+ # the contents will be readable and 0.
+ # If the flash encryption is enabled it is expected to have a valid
+ # non-zero key. We break out on first occurance of non-zero value
+ key_word = [0] * 7
+ for i in range(len(key_word)):
+ key_word[i] = self.read_efuse(14 + i)
+ # key is non-zero so break & return
+ if key_word[i] != 0:
+ return True
+ return False
+
+ def get_flash_crypt_config(self):
+ """For flash encryption related commands we need to make sure
+ user has programmed all the relevant efuse correctly so before
+ writing encrypted write_flash_encrypt esptool will verify the values
+ of flash_crypt_config to be non zero if they are not read
+ protected. If the values are zero a warning will be printed
+
+ bit 3 in efuse_rd_disable[3:0] is mapped to flash_crypt_config
+ this bit is at position 19 in EFUSE_BLK0_RDATA0_REG"""
+ word0 = self.read_efuse(0)
+ rd_disable = (word0 >> 19) & 0x1
+
+ if rd_disable == 0:
+ """we can read the flash_crypt_config efuse value
+ so go & read it (EFUSE_BLK0_RDATA5_REG[31:28])"""
+ word5 = self.read_efuse(5)
+ word5 = (word5 >> 28) & 0xF
+ return word5
+ else:
+ # if read of the efuse is disabled we assume it is set correctly
+ return 0xF
+
+ def get_encrypted_download_disabled(self):
+ return (
+ self.read_reg(self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG)
+ & self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT
+ )
+
+ def get_flash_encryption_enabled(self):
+ flash_crypt_cnt = (
+ self.read_reg(self.EFUSE_SPI_BOOT_CRYPT_CNT_REG)
+ & self.EFUSE_SPI_BOOT_CRYPT_CNT_MASK
+ )
+ # Flash encryption enabled when odd number of bits are set
+ return bin(flash_crypt_cnt).count("1") & 1 != 0
+
+ def get_secure_boot_enabled(self):
+ efuses = self.read_reg(self.EFUSE_RD_ABS_DONE_REG)
+ rev = self.get_chip_revision()
+ return efuses & self.EFUSE_RD_ABS_DONE_0_MASK or (
+ rev >= 300 and efuses & self.EFUSE_RD_ABS_DONE_1_MASK
+ )
+
+ def get_pkg_version(self):
+ word3 = self.read_efuse(3)
+ pkg_version = (word3 >> 9) & 0x07
+ pkg_version += ((word3 >> 2) & 0x1) << 3
+ return pkg_version
+
+ def get_chip_revision(self):
+ return self.get_major_chip_version() * 100 + self.get_minor_chip_version()
+
+ def get_minor_chip_version(self):
+ return (self.read_efuse(5) >> 24) & 0x3
+
+ def get_major_chip_version(self):
+ rev_bit0 = (self.read_efuse(3) >> 15) & 0x1
+ rev_bit1 = (self.read_efuse(5) >> 20) & 0x1
+ apb_ctl_date = self.read_reg(self.APB_CTL_DATE_ADDR)
+ rev_bit2 = (apb_ctl_date >> self.APB_CTL_DATE_S) & self.APB_CTL_DATE_V
+ combine_value = (rev_bit2 << 2) | (rev_bit1 << 1) | rev_bit0
+
+ revision = {
+ 0: 0,
+ 1: 1,
+ 3: 2,
+ 7: 3,
+ }.get(combine_value, 0)
+ return revision
+
+ def get_chip_description(self):
+ pkg_version = self.get_pkg_version()
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ rev3 = major_rev == 3
+ single_core = self.read_efuse(3) & (1 << 0) # CHIP_VER DIS_APP_CPU
+
+ chip_name = {
+ 0: "ESP32-S0WDQ6" if single_core else "ESP32-D0WDQ6",
+ 1: "ESP32-S0WD" if single_core else "ESP32-D0WD",
+ 2: "ESP32-D2WD",
+ 4: "ESP32-U4WDH",
+ 5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4",
+ 6: "ESP32-PICO-V3-02",
+ 7: "ESP32-D0WDR2-V3",
+ }.get(pkg_version, "unknown ESP32")
+
+ # ESP32-D0WD-V3, ESP32-D0WDQ6-V3
+ if chip_name.startswith("ESP32-D0WD") and rev3:
+ chip_name += "-V3"
+
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ features = ["WiFi"]
+ word3 = self.read_efuse(3)
+
+ # names of variables in this section are lowercase
+ # versions of EFUSE names as documented in TRM and
+ # ESP-IDF efuse_reg.h
+
+ chip_ver_dis_bt = word3 & (1 << 1)
+ if chip_ver_dis_bt == 0:
+ features += ["BT"]
+
+ chip_ver_dis_app_cpu = word3 & (1 << 0)
+ if chip_ver_dis_app_cpu:
+ features += ["Single Core"]
+ else:
+ features += ["Dual Core"]
+
+ chip_cpu_freq_rated = word3 & (1 << 13)
+ if chip_cpu_freq_rated:
+ chip_cpu_freq_low = word3 & (1 << 12)
+ if chip_cpu_freq_low:
+ features += ["160MHz"]
+ else:
+ features += ["240MHz"]
+
+ pkg_version = self.get_pkg_version()
+ if pkg_version in [2, 4, 5, 6]:
+ features += ["Embedded Flash"]
+
+ if pkg_version == 6:
+ features += ["Embedded PSRAM"]
+
+ word4 = self.read_efuse(4)
+ adc_vref = (word4 >> 8) & 0x1F
+ if adc_vref:
+ features += ["VRef calibration in efuse"]
+
+ blk3_part_res = word3 >> 14 & 0x1
+ if blk3_part_res:
+ features += ["BLK3 partially reserved"]
+
+ word6 = self.read_efuse(6)
+ coding_scheme = word6 & 0x3
+ features += [
+ "Coding Scheme %s"
+ % {0: "None", 1: "3/4", 2: "Repeat (UNSUPPORTED)", 3: "Invalid"}[
+ coding_scheme
+ ]
+ ]
+
+ return features
+
+ def read_efuse(self, n):
+ """Read the nth word of the ESP3x EFUSE region."""
+ return self.read_reg(self.EFUSE_RD_REG_BASE + (4 * n))
+
+ def chip_id(self):
+ raise NotSupportedError(self, "chip_id")
+
+ def read_mac(self):
+ """Read MAC from EFUSE region"""
+ words = [self.read_efuse(2), self.read_efuse(1)]
+ bitstring = struct.pack(">II", *words)
+ bitstring = bitstring[2:8] # trim the 2 byte CRC
+ return tuple(bitstring)
+
+ def get_erase_size(self, offset, size):
+ return size
+
+ def override_vddsdio(self, new_voltage):
+ new_voltage = new_voltage.upper()
+ if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES:
+ raise FatalError(
+ "The only accepted VDDSDIO overrides are '1.8V', '1.9V' and 'OFF'"
+ )
+ RTC_CNTL_SDIO_CONF_REG = 0x3FF48074
+ RTC_CNTL_XPD_SDIO_REG = 1 << 31
+ RTC_CNTL_DREFH_SDIO_M = 3 << 29
+ RTC_CNTL_DREFM_SDIO_M = 3 << 27
+ RTC_CNTL_DREFL_SDIO_M = 3 << 25
+ # RTC_CNTL_SDIO_TIEH = (1 << 23)
+ # not used here, setting TIEH=1 would set 3.3V output,
+ # not safe for esptool.py to do
+ RTC_CNTL_SDIO_FORCE = 1 << 22
+ RTC_CNTL_SDIO_PD_EN = 1 << 21
+
+ reg_val = RTC_CNTL_SDIO_FORCE # override efuse setting
+ reg_val |= RTC_CNTL_SDIO_PD_EN
+ if new_voltage != "OFF":
+ reg_val |= RTC_CNTL_XPD_SDIO_REG # enable internal LDO
+ if new_voltage == "1.9V":
+ reg_val |= (
+ RTC_CNTL_DREFH_SDIO_M | RTC_CNTL_DREFM_SDIO_M | RTC_CNTL_DREFL_SDIO_M
+ ) # boost voltage
+ self.write_reg(RTC_CNTL_SDIO_CONF_REG, reg_val)
+ print("VDDSDIO regulator set to %s" % new_voltage)
+
+ def read_flash_slow(self, offset, length, progress_fn):
+ BLOCK_LEN = 64 # ROM read limit per command (this limit is why it's so slow)
+
+ data = b""
+ while len(data) < length:
+ block_len = min(BLOCK_LEN, length - len(data))
+ r = self.check_command(
+ "read flash block",
+ self.ESP_READ_FLASH_SLOW,
+ struct.pack("> self.TIMERS_RTC_CALI_VALUE_S
+ ) & self.TIMERS_RTC_CALI_VALUE
+ clk_8M_freq = self.read_efuse(4) & (0xFF) # EFUSE_RD_CK8M_FREQ
+ rom_calculated_freq = cali_val * 15625 * clk_8M_freq / 40
+ return rom_calculated_freq
+
+ def change_baud(self, baud):
+ # It's a workaround to avoid esp32 CK_8M frequency drift.
+ rom_calculated_freq = self.get_rom_cal_crystal_freq()
+ valid_freq = 40000000 if rom_calculated_freq > 33000000 else 26000000
+ false_rom_baud = int(baud * rom_calculated_freq // valid_freq)
+
+ print(f"Changing baud rate to {baud}")
+ self.command(self.ESP_CHANGE_BAUDRATE, struct.pack("> 22) & 0x07
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C2",
+ 1: "ESP32-C2",
+ }.get(self.get_pkg_version(), "unknown ESP32-C2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_minor_chip_version(self):
+ num_word = 1
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 16) & 0xF
+
+ def get_major_chip_version(self):
+ num_word = 1
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 20) & 0x3
+
+ def get_crystal_freq(self):
+ # The crystal detection algorithm of ESP32/ESP8266 works for ESP32-C2 as well.
+ return ESPLoader.get_crystal_freq(self)
+
+ def change_baud(self, baud):
+ rom_with_26M_XTAL = not self.IS_STUB and self.get_crystal_freq() == 26
+ if rom_with_26M_XTAL:
+ # The code is copied over from ESPLoader.change_baud().
+ # Probably this is just a temporary solution until the next chip revision.
+
+ # The ROM code thinks it uses a 40 MHz XTAL. Recompute the baud rate
+ # in order to trick the ROM code to set the correct baud rate for
+ # a 26 MHz XTAL.
+ false_rom_baud = baud * 40 // 26
+
+ print(f"Changing baud rate to {baud}")
+ self.command(
+ self.ESP_CHANGE_BAUDRATE, struct.pack("> 21) & 0x07
+
+ def get_minor_chip_version(self):
+ hi_num_word = 5
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+ low_num_word = 3
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+ return (hi << 3) + low
+
+ def get_major_chip_version(self):
+ num_word = 5
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C3",
+ }.get(self.get_pkg_version(), "unknown ESP32-C3")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["WiFi", "BLE"]
+
+ def get_crystal_freq(self):
+ # ESP32C3 XTAL is fixed to 40MHz
+ return 40
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-C3"
+ )
+
+ def read_mac(self):
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-C3
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > 5:
+ raise FatalError("Valid key block numbers must be in range 0-5")
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see an AES-128 key
+ purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+ return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
+
+ def change_baud(self, baud):
+ ESPLoader.change_baud(self, baud)
+
+ def uses_usb_jtag_serial(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-JTAG/Serial is being used
+ """
+ if self.secure_download_mode:
+ return False # Can't detect USB-JTAG/Serial in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_JTAG_SERIAL
+
+ def disable_rtc_watchdog(self):
+ # When USB-JTAG/Serial is used, the RTC watchdog is not reset
+ # and can then reset the board during flashing. Disable it.
+ if self.uses_usb_jtag_serial():
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY)
+ self.write_reg(self.RTC_CNTL_WDTCONFIG0_REG, 0)
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0)
+
+ def _post_connect(self):
+ if not self.sync_stub_detected: # Don't run if stub is reused
+ self.disable_rtc_watchdog()
+
+
+class ESP32C3StubLoader(ESP32C3ROM):
+ """Access class for ESP32C3 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32c6.py b/installer/bin/esptool/esptool/targets/esp32c6.py
new file mode 100644
index 0000000..0c4575e
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32c6.py
@@ -0,0 +1,189 @@
+# SPDX-FileCopyrightText: 2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+
+from .esp32c3 import ESP32C3ROM
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32C6ROM(ESP32C3ROM):
+ CHIP_NAME = "ESP32-C6"
+ IMAGE_CHIP_ID = 13
+
+ FPGA_SLOW_BOOT = False
+
+ IROM_MAP_START = 0x42000000
+ IROM_MAP_END = 0x42800000
+ DROM_MAP_START = 0x42800000
+ DROM_MAP_END = 0x43000000
+
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ # Magic value for ESP32C6
+ CHIP_DETECT_MAGIC_VALUE = [0x2CE0806F]
+
+ SPI_REG_BASE = 0x60003000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ UART_DATE_REG_ADDR = 0x60000000 + 0x7C
+
+ EFUSE_BASE = 0x600B0800
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ UARTDEV_BUF_NO = 0x4087F580 # Variable in ROM .bss which indicates the port in use
+ UARTDEV_BUF_NO_USB_JTAG_SERIAL = 3 # The above var when USB-JTAG/Serial is used
+
+ DR_REG_LP_WDT_BASE = 0x600B1C00
+ RTC_CNTL_WDTCONFIG0_REG = DR_REG_LP_WDT_BASE + 0x0 # LP_WDT_RWDT_CONFIG0_REG
+ RTC_CNTL_WDTWPROTECT_REG = DR_REG_LP_WDT_BASE + 0x0018 # LP_WDT_RWDT_WPROTECT_REG
+
+ FLASH_FREQUENCY = {
+ "80m": 0x0, # workaround for wrong mspi HS div value in ROM
+ "40m": 0x0,
+ "20m": 0x2,
+ }
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x42800000, 0x43000000, "DROM"],
+ [0x40800000, 0x40880000, "DRAM"],
+ [0x40800000, 0x40880000, "BYTE_ACCESSIBLE"],
+ [0x4004AC00, 0x40050000, "DROM_MASK"],
+ [0x40000000, 0x4004AC00, "IROM_MASK"],
+ [0x42000000, 0x42800000, "IROM"],
+ [0x40800000, 0x40880000, "IRAM"],
+ [0x50000000, 0x50004000, "RTC_IRAM"],
+ [0x50000000, 0x50004000, "RTC_DRAM"],
+ [0x600FE000, 0x60100000, "MEM_INTERNAL2"],
+ ]
+
+ def get_pkg_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
+
+ def get_minor_chip_version(self):
+ hi_num_word = 5
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+ low_num_word = 3
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+ return (hi << 3) + low
+
+ def get_major_chip_version(self):
+ num_word = 5
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C6",
+ }.get(self.get_pkg_version(), "unknown ESP32-C6")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["WiFi 6", "BT 5", "IEEE802.15.4"]
+
+ def get_crystal_freq(self):
+ # ESP32C6 XTAL is fixed to 40MHz
+ return 40
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-C6"
+ )
+
+ def read_mac(self):
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-C6
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > 5:
+ raise FatalError("Valid key block numbers must be in range 0-5")
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see an AES-128 key
+ purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+ return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
+
+
+class ESP32C6StubLoader(ESP32C6ROM):
+ """Access class for ESP32C6 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32C6ROM.STUB_CLASS = ESP32C6StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32c6beta.py b/installer/bin/esptool/esptool/targets/esp32c6beta.py
new file mode 100644
index 0000000..4c4b6be
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32c6beta.py
@@ -0,0 +1,26 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32c3 import ESP32C3ROM
+
+
+class ESP32C6BETAROM(ESP32C3ROM):
+ CHIP_NAME = "ESP32-C6(beta)"
+ IMAGE_CHIP_ID = 7
+
+ CHIP_DETECT_MAGIC_VALUE = [0x0DA1806F]
+
+ UART_DATE_REG_ADDR = 0x00000500
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C6",
+ }.get(self.get_pkg_version(), "unknown ESP32-C6")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def _post_connect(self):
+ pass
diff --git a/installer/bin/esptool/esptool/targets/esp32h2.py b/installer/bin/esptool/esptool/targets/esp32h2.py
new file mode 100644
index 0000000..a37617e
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32h2.py
@@ -0,0 +1,65 @@
+# SPDX-FileCopyrightText: 2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32c6 import ESP32C6ROM
+
+
+class ESP32H2ROM(ESP32C6ROM):
+ CHIP_NAME = "ESP32-H2"
+ IMAGE_CHIP_ID = 16
+
+ # Magic value for ESP32H2
+ CHIP_DETECT_MAGIC_VALUE = [0xD7B73E80]
+
+ FLASH_FREQUENCY = {
+ "48m": 0xF,
+ "24m": 0x0,
+ "16m": 0x1,
+ "12m": 0x2,
+ }
+
+ def get_pkg_version(self):
+ num_word = 3
+ block1_addr = self.EFUSE_BASE + 0x044
+ word3 = self.read_reg(block1_addr + (4 * num_word))
+ pkg_version = (word3 >> 21) & 0x0F
+ return pkg_version
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-H2",
+ }.get(self.get_pkg_version(), "unknown ESP32-H2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["BLE"]
+
+ def get_crystal_freq(self):
+ # ESP32H2 XTAL is fixed to 32MHz
+ return 32
+
+
+class ESP32H2StubLoader(ESP32H2ROM):
+ """Access class for ESP32H2 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32H2ROM.STUB_CLASS = ESP32H2StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32h2beta1.py b/installer/bin/esptool/esptool/targets/esp32h2beta1.py
new file mode 100644
index 0000000..66b6df9
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32h2beta1.py
@@ -0,0 +1,164 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+
+from .esp32c3 import ESP32C3ROM
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32H2BETA1ROM(ESP32C3ROM):
+ CHIP_NAME = "ESP32-H2(beta1)"
+ IMAGE_CHIP_ID = 10
+
+ IROM_MAP_START = 0x42000000
+ IROM_MAP_END = 0x42800000
+ DROM_MAP_START = 0x3C000000
+ DROM_MAP_END = 0x3C800000
+
+ SPI_REG_BASE = 0x60002000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ CHIP_DETECT_MAGIC_VALUE = [0xCA26CC22]
+
+ UART_DATE_REG_ADDR = 0x60000000 + 0x7C
+
+ EFUSE_BASE = 0x6001A000
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ MEMORY_MAP = []
+
+ FLASH_FREQUENCY = {
+ "48m": 0xF,
+ "24m": 0x0,
+ "16m": 0x1,
+ "12m": 0x2,
+ }
+
+ def get_pkg_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x0F
+
+ def get_minor_chip_version(self):
+ hi_num_word = 5
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+ low_num_word = 3
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+ return (hi << 3) + low
+
+ def get_major_chip_version(self):
+ num_word = 5
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-H2",
+ }.get(self.get_pkg_version(), "unknown ESP32-H2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["BLE", "IEEE802.15.4"]
+
+ def get_crystal_freq(self):
+ return 32
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-H2"
+ )
+
+ def read_mac(self):
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-H2
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > 5:
+ raise FatalError("Valid key block numbers must be in range 0-5")
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see an AES-128 key
+ purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+ return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
+
+ def _post_connect(self):
+ pass
+
+
+class ESP32H2BETA1StubLoader(ESP32H2BETA1ROM):
+ """Access class for ESP32H2BETA1 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32H2BETA1ROM.STUB_CLASS = ESP32H2BETA1StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32h2beta2.py b/installer/bin/esptool/esptool/targets/esp32h2beta2.py
new file mode 100644
index 0000000..6fa8f58
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32h2beta2.py
@@ -0,0 +1,43 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32h2beta1 import ESP32H2BETA1ROM
+
+
+class ESP32H2BETA2ROM(ESP32H2BETA1ROM):
+ CHIP_NAME = "ESP32-H2(beta2)"
+ IMAGE_CHIP_ID = 14
+
+ CHIP_DETECT_MAGIC_VALUE = [0x6881B06F]
+
+ def get_chip_description(self):
+ chip_name = {
+ 1: "ESP32-H2(beta2)",
+ }.get(self.get_pkg_version(), "unknown ESP32-H2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+
+class ESP32H2BETA2StubLoader(ESP32H2BETA2ROM):
+ """Access class for ESP32H2BETA2 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32H2BETA2ROM.STUB_CLASS = ESP32H2BETA2StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32s2.py b/installer/bin/esptool/esptool/targets/esp32s2.py
new file mode 100644
index 0000000..bffd553
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32s2.py
@@ -0,0 +1,305 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import struct
+
+from .esp32 import ESP32ROM
+from ..loader import ESPLoader
+from ..reset import HardReset
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32S2ROM(ESP32ROM):
+ CHIP_NAME = "ESP32-S2"
+ IMAGE_CHIP_ID = 2
+
+ FPGA_SLOW_BOOT = False
+
+ IROM_MAP_START = 0x40080000
+ IROM_MAP_END = 0x40B80000
+ DROM_MAP_START = 0x3F000000
+ DROM_MAP_END = 0x3F3F0000
+
+ CHIP_DETECT_MAGIC_VALUE = [0x000007C6]
+
+ SPI_REG_BASE = 0x3F402000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ MAC_EFUSE_REG = 0x3F41A044 # ESP32-S2 has special block for MAC efuses
+
+ UART_CLKDIV_REG = 0x3F400014
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ # todo: use espefuse APIs to get this info
+ EFUSE_BASE = 0x3F41A000
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x05C
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 19
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ EFUSE_RD_REPEAT_DATA3_REG = EFUSE_BASE + 0x3C
+ EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK = 1 << 9
+
+ PURPOSE_VAL_XTS_AES256_KEY_1 = 2
+ PURPOSE_VAL_XTS_AES256_KEY_2 = 3
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ UARTDEV_BUF_NO = 0x3FFFFD14 # Variable in ROM .bss which indicates the port in use
+ UARTDEV_BUF_NO_USB_OTG = 2 # Value of the above indicating that USB-OTG is in use
+
+ USB_RAM_BLOCK = 0x800 # Max block size USB-OTG is used
+
+ GPIO_STRAP_REG = 0x3F404038
+ GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode
+ RTC_CNTL_OPTION1_REG = 0x3F408128
+ RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB?
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x3F000000, 0x3FF80000, "DROM"],
+ [0x3F500000, 0x3FF80000, "EXTRAM_DATA"],
+ [0x3FF9E000, 0x3FFA0000, "RTC_DRAM"],
+ [0x3FF9E000, 0x40000000, "BYTE_ACCESSIBLE"],
+ [0x3FF9E000, 0x40072000, "MEM_INTERNAL"],
+ [0x3FFB0000, 0x40000000, "DRAM"],
+ [0x40000000, 0x4001A100, "IROM_MASK"],
+ [0x40020000, 0x40070000, "IRAM"],
+ [0x40070000, 0x40072000, "RTC_IRAM"],
+ [0x40080000, 0x40800000, "IROM"],
+ [0x50000000, 0x50002000, "RTC_DATA"],
+ ]
+
+ def get_pkg_version(self):
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
+
+ def get_minor_chip_version(self):
+ hi_num_word = 3
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 20) & 0x01
+ low_num_word = 4
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 4) & 0x07
+ return (hi << 3) + low
+
+ def get_major_chip_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x03
+
+ def get_flash_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x0F
+
+ def get_psram_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 28) & 0x0F
+
+ def get_block2_version(self):
+ # BLK_VERSION_MINOR
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 4) & 0x07
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-S2",
+ 1: "ESP32-S2FH2",
+ 2: "ESP32-S2FH4",
+ 102: "ESP32-S2FNR2",
+ 100: "ESP32-S2R2",
+ }.get(
+ self.get_flash_version() + self.get_psram_version() * 100,
+ "unknown ESP32-S2",
+ )
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ features = ["WiFi"]
+
+ if self.secure_download_mode:
+ features += ["Secure Download Mode Enabled"]
+
+ flash_version = {
+ 0: "No Embedded Flash",
+ 1: "Embedded Flash 2MB",
+ 2: "Embedded Flash 4MB",
+ }.get(self.get_flash_version(), "Unknown Embedded Flash")
+ features += [flash_version]
+
+ psram_version = {
+ 0: "No Embedded PSRAM",
+ 1: "Embedded PSRAM 2MB",
+ 2: "Embedded PSRAM 4MB",
+ }.get(self.get_psram_version(), "Unknown Embedded PSRAM")
+ features += [psram_version]
+
+ block2_version = {
+ 0: "No calibration in BLK2 of efuse",
+ 1: "ADC and temperature sensor calibration in BLK2 of efuse V1",
+ 2: "ADC and temperature sensor calibration in BLK2 of efuse V2",
+ }.get(self.get_block2_version(), "Unknown Calibration in BLK2")
+ features += [block2_version]
+
+ return features
+
+ def get_crystal_freq(self):
+ # ESP32-S2 XTAL is fixed to 40MHz
+ return 40
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-S2"
+ )
+
+ def read_mac(self):
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def flash_type(self):
+ return (
+ 1
+ if self.read_reg(self.EFUSE_RD_REPEAT_DATA3_REG)
+ & self.EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK
+ else 0
+ )
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-S2
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > 5:
+ raise FatalError("Valid key block numbers must be in range 0-5")
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see either an AES-128 key or two AES-256 keys
+ purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+ if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes):
+ return True
+
+ return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) and any(
+ p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes
+ )
+
+ def uses_usb_otg(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-OTG console is being used
+ """
+ if self.secure_download_mode:
+ return False # can't detect native USB in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_OTG
+
+ def _post_connect(self):
+ if self.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+
+ def _check_if_can_reset(self):
+ """
+ Check the strapping register to see if we can reset out of download mode.
+ """
+ if os.getenv("ESPTOOL_TESTING") is not None:
+ print("ESPTOOL_TESTING is set, ignoring strapping mode check")
+ # Esptool tests over USB-OTG run with GPIO0 strapped low,
+ # don't complain in this case.
+ return
+ strap_reg = self.read_reg(self.GPIO_STRAP_REG)
+ force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG)
+ if (
+ strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0
+ and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0
+ ):
+ print(
+ "WARNING: {} chip was placed into download mode using GPIO0.\n"
+ "esptool.py can not exit the download mode over USB. "
+ "To run the app, reset the chip manually.\n"
+ "To suppress this note, set --after option to 'no_reset'.".format(
+ self.get_chip_description()
+ )
+ )
+ raise SystemExit(1)
+
+ def hard_reset(self):
+ uses_usb_otg = self.uses_usb_otg()
+ if uses_usb_otg:
+ self._check_if_can_reset()
+
+ print("Hard resetting via RTS pin...")
+ HardReset(self._port, uses_usb_otg)()
+
+ def change_baud(self, baud):
+ ESPLoader.change_baud(self, baud)
+
+
+class ESP32S2StubLoader(ESP32S2ROM):
+ """Access class for ESP32-S2 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+ if rom_loader.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+ self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK
+
+
+ESP32S2ROM.STUB_CLASS = ESP32S2StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32s3.py b/installer/bin/esptool/esptool/targets/esp32s3.py
new file mode 100644
index 0000000..660537b
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32s3.py
@@ -0,0 +1,315 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import struct
+
+from .esp32 import ESP32ROM
+from ..loader import ESPLoader
+from ..reset import HardReset
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32S3ROM(ESP32ROM):
+ CHIP_NAME = "ESP32-S3"
+
+ IMAGE_CHIP_ID = 9
+
+ CHIP_DETECT_MAGIC_VALUE = [0x9]
+
+ FPGA_SLOW_BOOT = False
+
+ IROM_MAP_START = 0x42000000
+ IROM_MAP_END = 0x44000000
+ DROM_MAP_START = 0x3C000000
+ DROM_MAP_END = 0x3E000000
+
+ UART_DATE_REG_ADDR = 0x60000080
+
+ SPI_REG_BASE = 0x60002000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ # todo: use espefuse APIs to get this info
+ EFUSE_BASE = 0x60007000 # BLOCK0 read base address
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x44
+ EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x5C
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ EFUSE_RD_REPEAT_DATA3_REG = EFUSE_BASE + 0x3C
+ EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK = 1 << 9
+
+ PURPOSE_VAL_XTS_AES256_KEY_1 = 2
+ PURPOSE_VAL_XTS_AES256_KEY_2 = 3
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ UARTDEV_BUF_NO = 0x3FCEF14C # Variable in ROM .bss which indicates the port in use
+ UARTDEV_BUF_NO_USB_OTG = 3 # The above var when USB-OTG is used
+ UARTDEV_BUF_NO_USB_JTAG_SERIAL = 4 # The above var when USB-JTAG/Serial is used
+
+ RTC_CNTL_WDT_WKEY = 0x50D83AA1
+ RTCCNTL_BASE_REG = 0x60008000
+ RTC_CNTL_WDTCONFIG0_REG = RTCCNTL_BASE_REG + 0x0090
+ RTC_CNTL_WDTWPROTECT_REG = RTCCNTL_BASE_REG + 0x00B0
+
+ USB_RAM_BLOCK = 0x800 # Max block size USB-OTG is used
+
+ GPIO_STRAP_REG = 0x60004038
+ GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode
+ RTC_CNTL_OPTION1_REG = 0x6000812C
+ RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB?
+
+ UART_CLKDIV_REG = 0x60000014
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x3C000000, 0x3D000000, "DROM"],
+ [0x3D000000, 0x3E000000, "EXTRAM_DATA"],
+ [0x600FE000, 0x60100000, "RTC_DRAM"],
+ [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"],
+ [0x3FC88000, 0x403E2000, "MEM_INTERNAL"],
+ [0x3FC88000, 0x3FD00000, "DRAM"],
+ [0x40000000, 0x4001A100, "IROM_MASK"],
+ [0x40370000, 0x403E0000, "IRAM"],
+ [0x600FE000, 0x60100000, "RTC_IRAM"],
+ [0x42000000, 0x42800000, "IROM"],
+ [0x50000000, 0x50002000, "RTC_DATA"],
+ ]
+
+ def get_pkg_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
+
+ def is_eco0(self, minor_raw):
+ # Workaround: The major version field was allocated to other purposes
+ # when block version is v1.1.
+ # Luckily only chip v0.0 have this kind of block version and efuse usage.
+ return (
+ (minor_raw & 0x7) == 0
+ and self.get_blk_version_major() == 1
+ and self.get_blk_version_minor() == 1
+ )
+
+ def get_minor_chip_version(self):
+ minor_raw = self.get_raw_minor_chip_version()
+ if self.is_eco0(minor_raw):
+ return 0
+ return minor_raw
+
+ def get_raw_minor_chip_version(self):
+ hi_num_word = 5
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+ low_num_word = 3
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+ return (hi << 3) + low
+
+ def get_blk_version_major(self):
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 0) & 0x03
+
+ def get_blk_version_minor(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x07
+
+ def get_major_chip_version(self):
+ minor_raw = self.get_raw_minor_chip_version()
+ if self.is_eco0(minor_raw):
+ return 0
+ return self.get_raw_major_chip_version()
+
+ def get_raw_major_chip_version(self):
+ num_word = 5
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
+ def get_chip_description(self):
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{self.CHIP_NAME} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["WiFi", "BLE"]
+
+ def get_crystal_freq(self):
+ # ESP32S3 XTAL is fixed to 40MHz
+ return 40
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-S3
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > 5:
+ raise FatalError("Valid key block numbers must be in range 0-5")
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see either an AES-128 key or two AES-256 keys
+ purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+ if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes):
+ return True
+
+ return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) and any(
+ p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes
+ )
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-S3"
+ )
+
+ def read_mac(self):
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def flash_type(self):
+ return (
+ 1
+ if self.read_reg(self.EFUSE_RD_REPEAT_DATA3_REG)
+ & self.EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK
+ else 0
+ )
+
+ def uses_usb_otg(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-OTG console is being used
+ """
+ if self.secure_download_mode:
+ return False # can't detect native USB in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_OTG
+
+ def uses_usb_jtag_serial(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-JTAG/Serial is being used
+ """
+ if self.secure_download_mode:
+ return False # can't detect USB-JTAG/Serial in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_JTAG_SERIAL
+
+ def disable_rtc_watchdog(self):
+ # When USB-JTAG/Serial is used, the RTC watchdog is not reset
+ # and can then reset the board during flashing. Disable it.
+ if self.uses_usb_jtag_serial():
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY)
+ self.write_reg(self.RTC_CNTL_WDTCONFIG0_REG, 0)
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0)
+
+ def _post_connect(self):
+ if self.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+ if not self.sync_stub_detected: # Don't run if stub is reused
+ self.disable_rtc_watchdog()
+
+ def _check_if_can_reset(self):
+ """
+ Check the strapping register to see if we can reset out of download mode.
+ """
+ if os.getenv("ESPTOOL_TESTING") is not None:
+ print("ESPTOOL_TESTING is set, ignoring strapping mode check")
+ # Esptool tests over USB-OTG run with GPIO0 strapped low,
+ # don't complain in this case.
+ return
+ strap_reg = self.read_reg(self.GPIO_STRAP_REG)
+ force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG)
+ if (
+ strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0
+ and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0
+ ):
+ print(
+ "WARNING: {} chip was placed into download mode using GPIO0.\n"
+ "esptool.py can not exit the download mode over USB. "
+ "To run the app, reset the chip manually.\n"
+ "To suppress this note, set --after option to 'no_reset'.".format(
+ self.get_chip_description()
+ )
+ )
+ raise SystemExit(1)
+
+ def hard_reset(self):
+ uses_usb_otg = self.uses_usb_otg()
+ if uses_usb_otg:
+ self._check_if_can_reset()
+
+ print("Hard resetting via RTS pin...")
+ HardReset(self._port, uses_usb_otg)()
+
+ def change_baud(self, baud):
+ ESPLoader.change_baud(self, baud)
+
+
+class ESP32S3StubLoader(ESP32S3ROM):
+ """Access class for ESP32S3 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+ if rom_loader.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+ self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK
+
+
+ESP32S3ROM.STUB_CLASS = ESP32S3StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32s3beta2.py b/installer/bin/esptool/esptool/targets/esp32s3beta2.py
new file mode 100644
index 0000000..b7958bd
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32s3beta2.py
@@ -0,0 +1,42 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32s3 import ESP32S3ROM
+
+
+class ESP32S3BETA2ROM(ESP32S3ROM):
+ CHIP_NAME = "ESP32-S3(beta2)"
+ IMAGE_CHIP_ID = 4
+
+ CHIP_DETECT_MAGIC_VALUE = [0xEB004136]
+
+ EFUSE_BASE = 0x6001A000 # BLOCK0 read base address
+
+ def get_chip_description(self):
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{self.CHIP_NAME} (revision v{major_rev}.{minor_rev})"
+
+
+class ESP32S3BETA2StubLoader(ESP32S3BETA2ROM):
+ """Access class for ESP32S3 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32S3BETA2ROM.STUB_CLASS = ESP32S3BETA2StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp8266.py b/installer/bin/esptool/esptool/targets/esp8266.py
new file mode 100644
index 0000000..f996663
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp8266.py
@@ -0,0 +1,191 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from ..loader import ESPLoader
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP8266ROM(ESPLoader):
+ """Access class for ESP8266 ROM bootloader"""
+
+ CHIP_NAME = "ESP8266"
+ IS_STUB = False
+
+ CHIP_DETECT_MAGIC_VALUE = [0xFFF0C101]
+
+ # OTP ROM addresses
+ ESP_OTP_MAC0 = 0x3FF00050
+ ESP_OTP_MAC1 = 0x3FF00054
+ ESP_OTP_MAC3 = 0x3FF0005C
+
+ SPI_REG_BASE = 0x60000200
+ SPI_USR_OFFS = 0x1C
+ SPI_USR1_OFFS = 0x20
+ SPI_USR2_OFFS = 0x24
+ SPI_MOSI_DLEN_OFFS = None
+ SPI_MISO_DLEN_OFFS = None
+ SPI_W0_OFFS = 0x40
+
+ UART_CLKDIV_REG = 0x60000014
+
+ XTAL_CLK_DIVIDER = 2
+
+ FLASH_SIZES = {
+ "512KB": 0x00,
+ "256KB": 0x10,
+ "1MB": 0x20,
+ "2MB": 0x30,
+ "4MB": 0x40,
+ "2MB-c1": 0x50,
+ "4MB-c1": 0x60,
+ "8MB": 0x80,
+ "16MB": 0x90,
+ }
+
+ FLASH_FREQUENCY = {
+ "80m": 0xF,
+ "40m": 0x0,
+ "26m": 0x1,
+ "20m": 0x2,
+ }
+
+ BOOTLOADER_FLASH_OFFSET = 0
+
+ MEMORY_MAP = [
+ [0x3FF00000, 0x3FF00010, "DPORT"],
+ [0x3FFE8000, 0x40000000, "DRAM"],
+ [0x40100000, 0x40108000, "IRAM"],
+ [0x40201010, 0x402E1010, "IROM"],
+ ]
+
+ def get_efuses(self):
+ # Return the 128 bits of ESP8266 efuse as a single Python integer
+ result = self.read_reg(0x3FF0005C) << 96
+ result |= self.read_reg(0x3FF00058) << 64
+ result |= self.read_reg(0x3FF00054) << 32
+ result |= self.read_reg(0x3FF00050)
+ return result
+
+ def _get_flash_size(self, efuses):
+ # rX_Y = EFUSE_DATA_OUTX[Y]
+ r0_4 = (efuses & (1 << 4)) != 0
+ r3_25 = (efuses & (1 << 121)) != 0
+ r3_26 = (efuses & (1 << 122)) != 0
+ r3_27 = (efuses & (1 << 123)) != 0
+
+ if r0_4 and not r3_25:
+ if not r3_27 and not r3_26:
+ return 1
+ elif not r3_27 and r3_26:
+ return 2
+ if not r0_4 and r3_25:
+ if not r3_27 and not r3_26:
+ return 2
+ elif not r3_27 and r3_26:
+ return 4
+ return -1
+
+ def get_chip_description(self):
+ efuses = self.get_efuses()
+ is_8285 = (
+ efuses & ((1 << 4) | 1 << 80)
+ ) != 0 # One or the other efuse bit is set for ESP8285
+ if is_8285:
+ flash_size = self._get_flash_size(efuses)
+ max_temp = (
+ efuses & (1 << 5)
+ ) != 0 # This efuse bit identifies the max flash temperature
+ chip_name = {
+ 1: "ESP8285H08" if max_temp else "ESP8285N08",
+ 2: "ESP8285H16" if max_temp else "ESP8285N16",
+ }.get(flash_size, "ESP8285")
+ return chip_name
+ return "ESP8266EX"
+
+ def get_chip_features(self):
+ features = ["WiFi"]
+ if "ESP8285" in self.get_chip_description():
+ features += ["Embedded Flash"]
+ return features
+
+ def flash_spi_attach(self, hspi_arg):
+ if self.IS_STUB:
+ super(ESP8266ROM, self).flash_spi_attach(hspi_arg)
+ else:
+ # ESP8266 ROM has no flash_spi_attach command in serial protocol,
+ # but flash_begin will do it
+ self.flash_begin(0, 0)
+
+ def flash_set_parameters(self, size):
+ # not implemented in ROM, but OK to silently skip for ROM
+ if self.IS_STUB:
+ super(ESP8266ROM, self).flash_set_parameters(size)
+
+ def chip_id(self):
+ """
+ Read Chip ID from efuse - the equivalent of the SDK system_get_chip_id() func
+ """
+ id0 = self.read_reg(self.ESP_OTP_MAC0)
+ id1 = self.read_reg(self.ESP_OTP_MAC1)
+ return (id0 >> 24) | ((id1 & 0xFFFFFF) << 8)
+
+ def read_mac(self):
+ """Read MAC from OTP ROM"""
+ mac0 = self.read_reg(self.ESP_OTP_MAC0)
+ mac1 = self.read_reg(self.ESP_OTP_MAC1)
+ mac3 = self.read_reg(self.ESP_OTP_MAC3)
+ if mac3 != 0:
+ oui = ((mac3 >> 16) & 0xFF, (mac3 >> 8) & 0xFF, mac3 & 0xFF)
+ elif ((mac1 >> 16) & 0xFF) == 0:
+ oui = (0x18, 0xFE, 0x34)
+ elif ((mac1 >> 16) & 0xFF) == 1:
+ oui = (0xAC, 0xD0, 0x74)
+ else:
+ raise FatalError("Unknown OUI")
+ return oui + ((mac1 >> 8) & 0xFF, mac1 & 0xFF, (mac0 >> 24) & 0xFF)
+
+ def get_erase_size(self, offset, size):
+ """Calculate an erase size given a specific size in bytes.
+
+ Provides a workaround for the bootloader erase bug."""
+
+ sectors_per_block = 16
+ sector_size = self.FLASH_SECTOR_SIZE
+ num_sectors = (size + sector_size - 1) // sector_size
+ start_sector = offset // sector_size
+
+ head_sectors = sectors_per_block - (start_sector % sectors_per_block)
+ if num_sectors < head_sectors:
+ head_sectors = num_sectors
+
+ if num_sectors < 2 * head_sectors:
+ return (num_sectors + 1) // 2 * sector_size
+ else:
+ return (num_sectors - head_sectors) * sector_size
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "Overriding VDDSDIO setting only applies to ESP32"
+ )
+
+
+class ESP8266StubLoader(ESP8266ROM):
+ """Access class for ESP8266 stub loader, runs on top of ROM."""
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+ def get_erase_size(self, offset, size):
+ return size # stub doesn't have same size bug as ROM loader
+
+
+ESP8266ROM.STUB_CLASS = ESP8266StubLoader
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32.json
new file mode 100644
index 0000000..3e87d81
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1074521560,
+ "text": "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",
+ "text_start": 1074520064,
+ "data": "GOv8P9jnC0Bx6AtA8+wLQO3oC0CP6AtA7egLQEnpC0AG6gtAeOoLQCHqC0CB5wtAo+kLQPjpC0Bn6QtAmuoLQI7pC0Ca6gtAXegLQLPoC0Dt6AtASekLQHfoC0BM6wtAs+wLQKXmC0DX7AtApeYLQKXmC0Cl5gtApeYLQKXmC0Cl5gtApeYLQKXmC0Dz6gtApeYLQM3rC0Cz7AtA",
+ "data_start": 1073605544
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c2.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c2.json
new file mode 100644
index 0000000..07887d4
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c2.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077413304,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "GGvKP+AIOEAsCThAhAk4QCgKOECUCjhAQgo4QKgHOEDkCThAJAo4QJgJOEBYBzhAzAk4QFgHOEC6CDhA/gg4QCwJOECECThAzAg4QBIIOEBCCDhAyAg4QOYMOEAsCThArAs4QJoMOECkBjhAxAw4QKQGOECkBjhApAY4QKQGOECkBjhApAY4QKQGOECkBjhASAs4QKQGOEDICzhAmgw4QA==",
+ "data_start": 1070295976
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c3.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c3.json
new file mode 100644
index 0000000..8426a63
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c3.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077413532,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "IGvIP1YKOECmCjhA/go4QKILOEAODDhAvAs4QCIJOEBeCzhAngs4QBILOEDSCDhARgs4QNIIOEAwCjhAdgo4QKYKOED+CjhAQgo4QIYJOEC2CThAPgo4QGAOOECmCjhAJg04QBgOOEASCDhAQA44QBIIOEASCDhAEgg4QBIIOEASCDhAEgg4QBIIOEASCDhAwgw4QBIIOEBEDThAGA44QA==",
+ "data_start": 1070164912
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6.json
new file mode 100644
index 0000000..2bf3acc
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1082132112,
+ "text": "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",
+ "text_start": 1082130432,
+ "data": "HCuEQCoKgEB6CoBA0gqAQHYLgEDiC4BAkAuAQPYIgEAyC4BAcguAQOYKgECmCIBAGguAQKYIgEAECoBASgqAQHoKgEDSCoBAFgqAQFoJgECKCYBAEgqAQDQOgEB6CoBA+gyAQOwNgEDmB4BAFA6AQOYHgEDmB4BA5geAQOYHgEDmB4BA5geAQOYHgEDmB4BAlgyAQOYHgEAYDYBA7A2AQA==",
+ "data_start": 1082469292
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6beta.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6beta.json
new file mode 100644
index 0000000..4709f9d
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6beta.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077413318,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "GGvIP/gIOEBECThAnAk4QEAKOECsCjhAWgo4QMAHOED8CThAPAo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QP4MOEBECThAxAs4QLIMOEC8BjhA3Aw4QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAYAs4QLwGOEDgCzhAsgw4QA==",
+ "data_start": 1070164904
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2.json
new file mode 100644
index 0000000..f003a55
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1082132112,
+ "text": "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",
+ "text_start": 1082130432,
+ "data": "HCuDQCoKgEB6CoBA0gqAQHYLgEDiC4BAkAuAQPYIgEAyC4BAcguAQOYKgECmCIBAGguAQKYIgEAECoBASgqAQHoKgEDSCoBAFgqAQFoJgECKCYBAEgqAQDQOgEB6CoBA+gyAQOwNgEDmB4BAFA6AQOYHgEDmB4BA5geAQOYHgEDmB4BA5geAQOYHgEDmB4BAlgyAQOYHgEAYDYBA7A2AQA==",
+ "data_start": 1082403756
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta1.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta1.json
new file mode 100644
index 0000000..37d29e8
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta1.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077413318,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "GGvIP/gIOEBECThAnAk4QEAKOECsCjhAWgo4QMAHOED8CThAPAo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QP4MOEBECThAxAs4QLIMOEC8BjhA3Aw4QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAYAs4QLwGOEDgCzhAsgw4QA==",
+ "data_start": 1070164904
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta2.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta2.json
new file mode 100644
index 0000000..f7cc1ce
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta2.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077413318,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "GGvIP/gIOEBECThAnAk4QEAKOECsCjhAWgo4QMAHOED8CThAPAo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QP4MOEBECThAxAs4QLIMOEC8BjhA3Aw4QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAYAs4QLwGOEDgCzhAsgw4QA==",
+ "data_start": 1070164904
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s2.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s2.json
new file mode 100644
index 0000000..de8cdc2
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s2.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1073907696,
+ "text": "CAAAYBwAAGBIAP0/EAAAYDZBACH7/8AgADgCQfr/wCAAKAQgIJSc4kH4/0YEAAw4MIgBwCAAqAiIBKCgdOAIAAsiZgLohvT/IfH/wCAAOQId8AAA7Cv+P2Sr/T+EgAAAQEAAAKTr/T/wK/4/NkEAsfn/IKB0EBEgZQEBlhoGgfb/kqEBkJkRmpjAIAC4CZHz/6CgdJqIwCAAkhgAkJD0G8nAwPTAIADCWACam8AgAKJJAMAgAJIYAIHq/5CQ9ICA9IeZR4Hl/5KhAZCZEZqYwCAAyAmh5f+x4/+HnBfGAQB86Ica3sYIAMAgAIkKwCAAuQlGAgDAIAC5CsAgAIkJkdf/mogMCcAgAJJYAB3wAABUIEA/VDBAPzZBAJH9/8AgAIgJgIAkVkj/kfr/wCAAiAmAgCRWSP8d8AAAACwgQD8AIEA/AAAACDZBABARIKX8/yH6/wwIwCAAgmIAkfr/gfj/wCAAkmgAwCAAmAhWef/AIACIAnzygCIwICAEHfAAAAAAQDZBABARIOX7/xZq/4Hs/5H7/8AgAJJoAMAgAJgIVnn/HfAAAFgA/T////8ABCBAPzZBACH8/zhCFoMGEBEgZfj/FvoFDPgMBDeoDZgigJkQgqABkEiDQEB0EBEgJfr/EBEgJfP/iCIMG0CYEZCrAcwUgKsBse3/sJkQsez/wCAAkmsAkc7/wCAAomkAwCAAqAlWev8cCQwaQJqDkDPAmog5QokiHfAAAHDi+j8IIEA/hGIBQKRiAUA2YQAQESBl7f8x+f+9Aa0Dgfr/4AgATQoMEuzqiAGSogCQiBCJARARIOXx/5Hy/6CiAcAgAIgJoIggwCAAiQm4Aa0Dge7/4AgAoCSDHfAAAP8PAAA2QQCBxf8MGZJIADCcQZkokfv/ORgpODAwtJoiKjMwPEEMAilYOUgQESAl+P8tCowaIqDFHfAAAMxxAUA2QQBBtv9YNFAzYxZjBFgUWlNQXEFGAQAQESDl7P+IRKYYBIgkh6XvEBEgJeX/Fmr/qBTNA70CgfH/4AgAoKB0jEpSoMRSZAVYFDpVWRRYNDBVwFk0HfAA+Pz/P0QA/T9MAP0/ADIBQOwxAUAwMwFANmEAfMitAoeTLTH3/8YFAKgDDBwQsSCB9//gCACBK/+iAQCICOAIAKgDgfP/4AgA5hrcxgoAAABmAyYMA80BDCsyYQCB7v/gCACYAYHo/zeZDagIZhoIMeb/wCAAokMAmQgd8EAA/T8AAP0/jDEBQDZBACH8/4Hc/8gCqAix+v+B+//gCAAMCIkCHfBgLwFANkEAgf7/4AgAggoYDAmCyP4MEoApkx3w+Cv+P/Qr/j8YAEw/jABMP//z//82QQAQESDl/P8WWgSh+P+ICrzYgff/mAi8abH2/3zMwCAAiAuQkBTAiBCQiCDAIACJC4gKsfH/DDpgqhHAIACYC6CIEKHu/6CZEJCIIMAgAIkLHfAoKwFANkEAEBEgZff/vBqR0f+ICRuoqQmR0P8MCoqZIkkAgsjBDBmAqYOggHTMiqKvQKoiIJiTjPkQESAl8v/GAQCtAoHv/+AIAB3wNkEAoqDAEBEg5fr/HfAAADZBAIKgwK0Ch5IRoqDbEBEgZfn/oqDcRgQAAAAAgqDbh5IIEBEgJfj/oqDdEBEgpff/HfA2QQA6MsYCAKICACLCARARIKX7/zeS8B3wAAAAbFIAQIxyAUCMUgBADFMAQDYhIaLREIH6/+AIAEYLAAAADBRARBFAQ2PNBL0BrQKB9f/gCACgoHT8Ws0EELEgotEQgfH/4AgASiJAM8BWA/0iogsQIrAgoiCy0RCB7P/gCACtAhwLEBEgpff/LQOGAAAioGMd8AAAQCsBQDZBABARICXl/4y6gYj/iAiMSBARICXi/wwKgfj/4AgAHfAAAIQyAUC08QBAkDIBQMDxAEA2QQAQESDl4f+smjFc/4ziqAOB9//gCACiogDGBgAAAKKiAIH0/+AIAKgDgfP/4AgARgUAAAAsCoyCgfD/4AgAhgEAAIHs/+AIAB3w8CsBQDZBIWKhB8BmERpmWQYMBWLREK0FUmYaEBEgZfn/DBhAiBFHuAJGRACtBoG1/+AIAIYzAACSpB1Qc8DgmREamUB3Y4kJzQe9ASCiIIGu/+AIAJKkHeCZERqZoKB0iAmMigwIgmYWfQiGFQCSpB3gmREamYkJEBEgpeL/vQetARARICXm/xARIKXh/80HELEgYKYggZ3/4AgAkqQd4JkRGpmICXAigHBVgDe1tJKhB8CZERqZmAmAdcCXtwJG3f+G5/8MCIJGbKKkGxCqoIHM/+AIAFYK/7KiC6IGbBC7sBARIGWbAPfqEvZHD7KiDRC7sHq7oksAG3eG8f9867eawWZHCIImGje4Aoe1nCKiCxAisGC2IK0CgX3/4AgAEBEgJdj/rQIcCxARIKXb/xARICXX/wwaEBEgpef/HfAAAP0/T0hBSfwr/j9sgAJASDwBQDyDAkAIAAhgEIACQAwAAGA4QEA///8AACiBQD+MgAAAEEAAAAAs/j8QLP4/UAD9P1QA/T9cLP4/FAAAYPD//wD8K/4/ZCv9P3AA/T9c8gBAiNgAQNDxAECk8QBA1DIBQFgyAUCg5ABABHABQAB1AUCASQFA6DUBQOw7AUCAAAFAmCABQOxwAUBscQFADHEBQIQpAUB4dgFA4HcBQJR2AUAAMABAaAABQDbBACHR/wwKKaGB5v/gCAAQESClvP8W6gQx+P5B9/7AIAAoA1H3/ikEwCAAKAVh8f6ioGQpBmHz/mAiEGKkAGAiIMAgACkFgdj/4AgASAR8wkAiEAwkQCIgwCAAKQOGAQBJAksixgEAIbf/Mbj/DAQ3Mu0QESAlw/8MS6LBKBARIKXG/yKhARARIOXB/0H2/ZAiESokwCAASQIxrf8h3v0yYgAQESBls/8WOgYhov7Bov6oAgwrgaT+4AgADJw8CwwKgbr/4AgAsaP/DAwMmoG4/+AIAKKiAIE3/+AIALGe/6gCUqABgbP/4AgAqAKBLv/gCACoAoGw/+AIADGY/8AgACgDUCIgwCAAKQMGCgAAsZT/zQoMWoGm/+AIADGR/1KhAcAgACgDLApQIiDAIAApA4Eg/+AIAIGh/+AIACGK/8AgACgCzLocwzAiECLC+AwTIKODDAuBmv/gCADxg/8MHQwcsqAB4qEAQN0RAMwRgLsBoqAAgZP/4AgAIX7/KkQhDf5i0itGFwAAAFFs/sAgADIFADAwdBbDBKKiAMAgACJFAIEC/+AIAKKiccCqEYF+/+AIAIGE/+AIAHFt/3zowCAAOAd8+oAzEBCqAcAgADkHgX7/4AgAgX3/4AgAIKIggXz/4AgAwCAAKAQWsvkMB8AgADgEDBLAIAB5BCJBHCIDAQwoeYEiQR2CUQ8cN3cSIhxHdxIjZpIlIgMDcgMCgCIRcCIgZkIWKCPAIAAoAimBhgIAHCKGAAAADMIiUQ8QESAlpv8Mi6LBHBARIOWp/7IDAyIDAoC7ESBbICFG/yAg9FeyHKKgwBARIKWk/6Kg7hARICWk/xARIKWi/0bZ/wAAIgMBHEcnNzf2IhlG4QAiwi8gIHS2QgKGJQBxN/9wIqAoAqACACLC/iAgdBwnJ7cCBtgAcTL/cCKgKAKgAgAAAHLCMHBwdLZXxMbRACxJDAcioMCXFQLGzwB5gQxyrQcQESAlnf+tBxARIKWc/xARICWb/xARIOWa/7KgCKLBHCLC/xARICWe/1YS/cYtAAwSVqUvwsEQvQWtBYEu/+AIAFaqLgzLosEQEBEg5Zv/hpgADBJWdS2BKP/gCACgJYPGsgAmhQQMEsawACgjeDNwgiCAgLRW2P4QESDlbv96IpwKBvj/oKxBgR3/4AgAVkr9ctfwcKLAzCcGhgAAoID0Vhj+hgMAoKD1gRb/4AgAVjr7UHfADBUAVRFwosB3NeWGAwCgrEGBDf/gCABWavly1/BwosBWp/5GdgAADAcioMAmhQKGlAAMBy0HxpIAJrX1hmgADBImtQKGjAC4M6IjAnKgABARIOWS/6Ang4aHAAwZZrVciEMgqREMByKgwoe6AgaFALhToiMCkmENEBEg5Wj/mNGgl4OGDQAMGWa1MYhDIKkRDAcioMKHugJGegAoM7hTqCMgeIKZ0RARIOVl/yFd/QwImNGJYiLSK3kioJiDLQnGbQCRV/0MB6IJACKgxneaAkZsAHgjssXwIqDAt5cBKFkMB5Kg70YCAHqDgggYG3eAmTC3J/KCAwVyAwSAiBFwiCByAwYAdxGAdyCCAweAiAFwiCCAmcCCoMEMB5Aok8ZYAIE//SKgxpIIAH0JFlkVmDgMByKgyHcZAgZSAChYkkgARk0AHIkMBwwSlxUCBk0A+HPoY9hTyEO4M6gjgbT+4AgADAh9CqAogwZGAAAADBImRQLGQACoIwwLgav+4AgABh8AUJA0DAcioMB3GQLGPABQVEGLw3z4hg4AAKg8ieGZ0cnBgZv+4AgAyMGI4SgseByoDJIhDXByECYCDsAgANIqACAoMNAiECB3IMAgAHkKG5nCzBBXOcJGlf9mRQLGk/8MByKgwIYmAAwSJrUCxiEAIX7+iFN4I4kCIX3+eQIMAgYdAKF5/gwH2AoMGbLF8I0HLQfQKYOwiZMgiBAioMZ3mGDBc/59COgMIqDJtz5TsPAUIqDAVq8ELQiGAgAAKoOIaEsiiQeNCSD+wCp9tzLtFsjd+Qx5CkZ1/wAMEmaFFyFj/ogCjBiCoMgMB3kCIV/+eQIMEoAngwwHRgEAAAwHIqD/IKB0EBEgZWn/cKB0EBEgpWj/EBEgZWf/VvK6IgMBHCcnNx/2MgJG6P4iwv0gIHQM9ye3Asbk/nFO/nAioCgCoAIAAHKg0ncSX3Kg1HeSAgYhAEbd/gAAKDM4IxARICVW/40KVkq2oqJxwKoRieGBR/7gCABxP/6RQP7AIAB4B4jhcLQ1wHcRkHcQcLsgILuCrQgwu8KBTf7gCACio+iBO/7gCADGyP4AANhTyEO4M6gjEBEgZXP/BsT+sgMDIgMCgLsRILsgssvwosMYEBEg5T7/Rr3+AAAiAwNyAwKAIhFwIiCBO/7gCABxrPwiwvCIN4AiYxYyrYgXioKAjEGGAgCJ4RARICUq/4IhDpInBKYZBJgnl6jpEBEgJSL/Fmr/qBfNArLDGIEr/uAIAIw6MqDEOVc4FyozORc4NyAjwCk3gSX+4AgABqD+AAByAwIiwxgyAwMMGYAzEXAzIDLD8AYiAHEG/oE5/OgHOZHgiMCJQYgmDBmHswEMOZJhDeJhDBARICUi/4H+/ZjR6MGh/f3dCL0CmQHCwSTywRCJ4YEP/uAIALgmnQqokYjhoLvAuSagM8C4B6oiqEEMDKq7DBq5B5DKg4C7wMDQdFZ8AMLbgMCtk5w6rQiCYQ6SYQ0QESDlLf+I4ZjRgmcAUWv8eDWMo5CPMZCIwNYoAFY39tapADFm/CKgxylTRgAAjDmcB4Zt/hY3m1Fh/CKgyClVBmr+ADFe/CKgySlTBmf+AAAoI1ZSmRARIOVS/6KiccCqEYHS/eAIABARICU6/4Hk/eAIAAZd/gAAKDMW0pYQESBlUP+io+iByf3gCAAQESClN//gAgCGVP4AEBEg5Tb/HfAAADZBAJ0CgqDAKAOHmQ/MMgwShgcADAIpA3zihg8AJhIHJiIYhgMAAACCoNuAKSOHmSoMIikDfPJGCAAAACKg3CeZCgwSKQMtCAYEAAAAgqDdfPKHmQYMEikDIqDbHfAAAA==",
+ "text_start": 1073905664,
+ "data": "ZCv9PzaLAkDBiwJAhpACQEqMAkDjiwJASowCQKmMAkByjQJA5Y0CQI2NAkDAigJAC40CQGSNAkDMjAJACI4CQPaMAkAIjgJAr4sCQA6MAkBKjAJAqYwCQMeLAkACiwJAx44CQD2QAkDYiQJAZZACQNiJAkDYiQJA2IkCQNiJAkDYiQJA2IkCQNiJAkDYiQJAZI4CQNiJAkBZjwJAPZACQA==",
+ "data_start": 1073622012
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3.json
new file mode 100644
index 0000000..1a73fca
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077381684,
+ "text": "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",
+ "text_start": 1077379072,
+ "data": "ZCvKP5aNN0B7jjdAPJM3QAaPN0CbjjdABo83QGWPN0AykDdApZA3QE2QN0AhjTdAyI83QCSQN0CIjzdAx5A3QLKPN0DHkDdAaY43QMaON0AGjzdAZY83QIGON0BijTdAiJE3QAKTN0A+jDdAIpM3QD6MN0A+jDdAPow3QD6MN0A+jDdAPow3QD6MN0A+jDdAIpE3QD6MN0AdkjdAApM3QAQInwAAAAAAAAAYAQQIBQAAAAAAAAAIAQQIBgAAAAAAAAAAAQQIIQAAAAAAIAAAEQQI3AAAAAAAIAAAEQQIDAAAAAAAIAAAAQQIEgAAAAAAIAAAESAoDAAQAQAA",
+ "data_start": 1070279668
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3beta2.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3beta2.json
new file mode 100644
index 0000000..8621a93
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3beta2.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077381176,
+ "text": "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",
+ "text_start": 1077379072,
+ "data": "ZCvKP4KLN0APjDdA15A3QJqMN0AvjDdAmow3QPmMN0DGjTdAOY43QOGNN0ANizdAXI03QLiNN0AcjTdAXI43QEaNN0BcjjdA/Ys3QFqMN0CajDdA+Yw3QBWMN0BOizdAHI83QJuQN0AsijdAvZA3QCyKN0AsijdALIo3QCyKN0AsijdALIo3QCyKN0AsijdAto43QCyKN0CyjzdAm5A3QA==",
+ "data_start": 1070279668
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_8266.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_8266.json
new file mode 100644
index 0000000..8d42b24
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_8266.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1074843652,
+ "text": "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",
+ "text_start": 1074843648,
+ "data": "CIH+PwUFBAACAwcAAwMLALnXEEDv1xBAHdgQQLrYEEBo5xBAHtkQQHTZEEDA2RBAaOcQQILaEED/2hBAwNsQQGjnEEBo5xBAWNwQQGjnEEA33xBAAOAQQDvgEEBo5xBAaOcQQNfgEEBo5xBAv+EQQGXiEECj4xBAY+QQQDTlEEBo5xBAaOcQQGjnEEBo5xBAYuYQQGjnEEBX5xBAkN0QQI/YEECm5RBAq9oQQPzZEEBo5xBA7OYQQDHnEEBo5xBAaOcQQGjnEEBo5xBAaOcQQGjnEEBo5xBAaOcQQCLaEEBf2hBAvuUQQAEAAAACAAAAAwAAAAQAAAAFAAAABwAAAAkAAAANAAAAEQAAABkAAAAhAAAAMQAAAEEAAABhAAAAgQAAAMEAAAABAQAAgQEAAAECAAABAwAAAQQAAAEGAAABCAAAAQwAAAEQAAABGAAAASAAAAEwAAABQAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAgAAAAIAAAADAAAAAwAAAAQAAAAEAAAABQAAAAUAAAAGAAAABgAAAAcAAAAHAAAACAAAAAgAAAAJAAAACQAAAAoAAAAKAAAACwAAAAsAAAAMAAAADAAAAA0AAAANAAAAAAAAAAAAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAANAAAADwAAABEAAAATAAAAFwAAABsAAAAfAAAAIwAAACsAAAAzAAAAOwAAAEMAAABTAAAAYwAAAHMAAACDAAAAowAAAMMAAADjAAAAAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAAEAAAABAAAAAQAAAAEAAAABQAAAAUAAAAFAAAABQAAAAAAAAAAAAAAAAAAABAREgAIBwkGCgULBAwDDQIOAQ8AAQEAAAEAAAAEAAAA",
+ "data_start": 1073720488
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/util.py b/installer/bin/esptool/esptool/util.py
new file mode 100644
index 0000000..66d9bd4
--- /dev/null
+++ b/installer/bin/esptool/esptool/util.py
@@ -0,0 +1,174 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import re
+import struct
+import sys
+
+
+def byte(bitstr, index):
+ return bitstr[index]
+
+
+def mask_to_shift(mask):
+ """Return the index of the least significant bit in the mask"""
+ shift = 0
+ while mask & 0x1 == 0:
+ shift += 1
+ mask >>= 1
+ return shift
+
+
+def div_roundup(a, b):
+ """Return a/b rounded up to nearest integer,
+ equivalent result to int(math.ceil(float(int(a)) / float(int(b))), only
+ without possible floating point accuracy errors.
+ """
+ return (int(a) + int(b) - 1) // int(b)
+
+
+def flash_size_bytes(size):
+ """Given a flash size of the type passed in args.flash_size
+ (ie 512KB or 1MB) then return the size in bytes.
+ """
+ if "MB" in size:
+ return int(size[: size.index("MB")]) * 1024 * 1024
+ elif "KB" in size:
+ return int(size[: size.index("KB")]) * 1024
+ else:
+ raise FatalError("Unknown size %s" % size)
+
+
+def hexify(s, uppercase=True):
+ format_str = "%02X" if uppercase else "%02x"
+ return "".join(format_str % c for c in s)
+
+
+def pad_to(data, alignment, pad_character=b"\xFF"):
+ """Pad to the next alignment boundary"""
+ pad_mod = len(data) % alignment
+ if pad_mod != 0:
+ data += pad_character * (alignment - pad_mod)
+ return data
+
+
+def print_overwrite(message, last_line=False):
+ """Print a message, overwriting the currently printed line.
+
+ If last_line is False, don't append a newline at the end
+ (expecting another subsequent call will overwrite this one.)
+
+ After a sequence of calls with last_line=False, call once with last_line=True.
+
+ If output is not a TTY (for example redirected a pipe),
+ no overwriting happens and this function is the same as print().
+ """
+ if sys.stdout.isatty():
+ print("\r%s" % message, end="\n" if last_line else "")
+ else:
+ print(message)
+
+
+def expand_chip_name(chip_name):
+ """Change chip name to official form, e.g. `esp32s3beta2` -> `ESP32-S3(beta2)`"""
+ # Put "-" after "esp32"
+ chip_name = re.sub(r"(esp32)(?!$)", r"\1-", chip_name)
+ # Put "()" around "betaN"
+ chip_name = re.sub(r"(beta\d*)", r"(\1)", chip_name)
+ # Uppercase everything before "(betaN)"
+ chip_name = re.sub(r"^[^\(]+", lambda x: x.group(0).upper(), chip_name)
+ return chip_name
+
+
+def strip_chip_name(chip_name):
+ """Strip chip name to normalized form, e.g. `ESP32-S3(beta2)` -> `esp32s3beta2`"""
+ return re.sub(r"[-()]", "", chip_name.lower())
+
+
+class FatalError(RuntimeError):
+ """
+ Wrapper class for runtime errors that aren't caused by internal bugs, but by
+ ESP ROM responses or input content.
+ """
+
+ def __init__(self, message):
+ RuntimeError.__init__(self, message)
+
+ @staticmethod
+ def WithResult(message, result):
+ """
+ Return a fatal error object that appends the hex values of
+ 'result' and its meaning as a string formatted argument.
+ """
+
+ err_defs = {
+ # ROM error codes
+ 0x101: "Out of memory",
+ 0x102: "Invalid argument",
+ 0x103: "Invalid state",
+ 0x104: "Invalid size",
+ 0x105: "Requested resource not found",
+ 0x106: "Operation or feature not supported",
+ 0x107: "Operation timed out",
+ 0x108: "Received response was invalid",
+ 0x109: "CRC or checksum was invalid",
+ 0x10A: "Version was invalid",
+ 0x10B: "MAC address was invalid",
+ # Flasher stub error codes
+ 0xC000: "Bad data length",
+ 0xC100: "Bad data checksum",
+ 0xC200: "Bad blocksize",
+ 0xC300: "Invalid command",
+ 0xC400: "Failed SPI operation",
+ 0xC500: "Failed SPI unlock",
+ 0xC600: "Not in flash mode",
+ 0xC700: "Inflate error",
+ 0xC800: "Not enough data",
+ 0xC900: "Too much data",
+ 0xFF00: "Command not implemented",
+ }
+
+ err_code = struct.unpack(">H", result[:2])
+ message += " (result was {}: {})".format(
+ hexify(result), err_defs.get(err_code[0], "Unknown result")
+ )
+ return FatalError(message)
+
+
+class NotImplementedInROMError(FatalError):
+ """
+ Wrapper class for the error thrown when a particular ESP bootloader function
+ is not implemented in the ROM bootloader.
+ """
+
+ def __init__(self, bootloader, func):
+ FatalError.__init__(
+ self,
+ "%s ROM does not support function %s."
+ % (bootloader.CHIP_NAME, func.__name__),
+ )
+
+
+class NotSupportedError(FatalError):
+ def __init__(self, esp, function_name):
+ FatalError.__init__(
+ self,
+ "Function %s is not supported for %s." % (function_name, esp.CHIP_NAME),
+ )
+
+
+class UnsupportedCommandError(RuntimeError):
+ """
+ Wrapper class for when ROM loader returns an invalid command response.
+
+ Usually this indicates the loader is running in Secure Download Mode.
+ """
+
+ def __init__(self, esp, op):
+ if esp.secure_download_mode:
+ msg = "This command (0x%x) is not supported in Secure Download Mode" % op
+ else:
+ msg = "Invalid (unsupported) command 0x%x" % op
+ RuntimeError.__init__(self, msg)
diff --git a/installer/install_upgrade.bat b/installer/install_upgrade.bat
new file mode 100644
index 0000000..bc95ac2
--- /dev/null
+++ b/installer/install_upgrade.bat
@@ -0,0 +1,10 @@
+@echo off
+echo Your firmware will be upgraded without factory reset.
+echo IF THIS IS YOUR FIRST FLASH ON THIS BOARD PLEASE USE install_with_factory_reset.bat INSTEAD!
+
+set /p port="Enter COM port (for example COM5): "
+
+python.exe bin/esptool/esptool.py --chip esp32 --port "%port%" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 0x1000 firmware/bootloader.bin 0x8000 firmware/partitions.bin 0xe000 firmware/boot_app0.bin 0x10000 firmware/firmware.bin
+
+echo "Firmware flashed"
+pause
\ No newline at end of file
diff --git a/installer/install_upgrade.sh b/installer/install_upgrade.sh
new file mode 100644
index 0000000..81a238e
--- /dev/null
+++ b/installer/install_upgrade.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+echo "Your firmware will be upgraded without factory reset."
+echo "IF THIS IS YOUR FIRST FLASH ON THIS BOARD PLEASE USE install_with_factory_reset.sh INSTEAD!"
+
+read -p "Enter COM port (for example /dev/ttyS5): " port
+
+python3 ./bin/esptool/esptool.py --chip esp32 --port "$port" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 0x1000 firmware/bootloader.bin 0x8000 firmware/partitions.bin 0xe000 firmware/boot_app0.bin 0x10000 firmware/firmware.bin
+
+echo "Firmware flashed"
\ No newline at end of file
diff --git a/installer/install_with_factory_reset.bat b/installer/install_with_factory_reset.bat
new file mode 100644
index 0000000..03f318c
--- /dev/null
+++ b/installer/install_with_factory_reset.bat
@@ -0,0 +1,14 @@
+@echo off
+echo Your current configuration will be LOST!!!
+echo Your current configuration will be LOST!!!
+echo Your current configuration will be LOST!!!
+echo If you already have this board flashed with our firmware please use install_upgrade.bat instead!
+
+set /p port="Enter COM port (for example COM5): "
+
+python.exe bin/esptool/esptool.py --chip esp32 --port "%port%" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 2686976 firmware/spiffs.bin
+
+python.exe bin/esptool/esptool.py --chip esp32 --port "%port%" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 0x1000 firmware/bootloader.bin 0x8000 firmware/partitions.bin 0xe000 firmware/boot_app0.bin 0x10000 firmware/firmware.bin
+
+echo "Firmware flashed"
+pause
\ No newline at end of file
diff --git a/installer/install_with_factory_reset.sh b/installer/install_with_factory_reset.sh
new file mode 100644
index 0000000..e69de29
diff --git a/platformio.ini b/platformio.ini
index 6e01e24..f3bbf1c 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -15,6 +15,12 @@ default_envs = ttgo-lora32-v21
platform = espressif32 @ 6.3.1
framework = arduino
monitor_speed = 115200
+board_build.embed_files =
+ data_embed/index.html.gz
+ data_embed/style.css.gz
+ data_embed/script.js.gz
+ data_embed/bootstrap.css.gz
+ data_embed/bootstrap.js.gz
lib_deps =
bblanchon/ArduinoJson@^6.20.2
sandeepmistry/LoRa@^0.8.0
@@ -28,9 +34,11 @@ lib_deps =
jgromes/RadioLib @ 6.1.0
lewisxhe/XPowersLib@^0.1.8
ayushsharma82/ElegantOTA@^3.1.0
- ottowinter/ESPAsyncWebServer-esphome@ 3.0.0
- esphome/AsyncTCP-esphome@ 2.1.1
+ ottowinter/ESPAsyncWebServer-esphome@3.0.0
+ esphome/AsyncTCP-esphome@2.1.1
+extra_scripts =
+ pre:tools/compress.py
[env:ttgo-lora32-v21]
board = ttgo-lora32-v21
@@ -67,4 +75,11 @@ build_flags = -Werror -Wall -DTTGO_T_Beam_V1_0_SX1268 -DELEGANTOTA_USE_ASYNC_WEB
[env:ttgo-t-beam-v1_2_SX1262]
board = ttgo-t-beam
-build_flags = -Werror -Wall -DTTGO_T_Beam_V1_2_SX1262 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
\ No newline at end of file
+build_flags = -Werror -Wall -DTTGO_T_Beam_V1_2_SX1262 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
+
+[env:heltec_wireless_stick_lite]
+platform = espressif32
+board = heltec_wireless_stick_lite
+board_build.mcu = esp32c3
+board_build.f_cpu = 240000000L
+build_flags = -Werror -Wall -DHELTEC_CT62 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
\ No newline at end of file
diff --git a/src/LoRa_APRS_iGate.cpp b/src/LoRa_APRS_iGate.cpp
index 8d4d6d8..6d03f0d 100644
--- a/src/LoRa_APRS_iGate.cpp
+++ b/src/LoRa_APRS_iGate.cpp
@@ -13,6 +13,7 @@
#include "digi_utils.h"
#include "gps_utils.h"
#include "bme_utils.h"
+#include "web_utils.h"
#include "display.h"
#include "utils.h"
@@ -20,7 +21,7 @@
Configuration Config;
WiFiClient espClient;
-String versionDate = "2024.01.28";
+String versionDate = "2024.02.23";
int myWiFiAPIndex = 0;
int myWiFiAPSize = Config.wifiAPs.size();
WiFi_AP *currentWiFi = &Config.wifiAPs[myWiFiAPIndex];
@@ -34,8 +35,12 @@ uint32_t lastScreenOn = millis();
uint32_t lastWiFiCheck = 0;
bool WiFiConnect = true;
+bool WiFiConnected = false;
int lastStationModeState = 1;
+bool WiFiAutoAPStarted = false;
+long WiFiAutoAPTime = false;
+
String batteryVoltage;
std::vector lastHeardStation;
@@ -46,55 +51,63 @@ std::vector packetBuffer_temp;
String firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, iGateBeaconPacket, iGateLoRaBeaconPacket;
void setup() {
- Serial.begin(115200);
- #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2)
- pinMode(batteryPin, INPUT);
- #endif
- #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
- pinMode(internalLedPin, OUTPUT);
- #endif
- if (Config.externalVoltageMeasurement) {
- pinMode(Config.externalVoltagePin, INPUT);
- }
- #if defined(TTGO_T_Beam_V1_0) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2) || defined(TTGO_T_Beam_V1_2_SX1262)
- POWER_Utils::setup();
- #endif
- delay(1000);
- Utils::setupDisplay();
- WIFI_Utils::setup();
- LoRa_Utils::setup();
- Utils::validateDigiFreqs();
- iGateBeaconPacket = GPS_Utils::generateBeacon();
- iGateLoRaBeaconPacket = GPS_Utils::generateiGateLoRaBeacon();
- Utils::startServer();
- SYSLOG_Utils::setup();
- BME_Utils::setup();
+ Serial.begin(115200);
+
+ #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2)
+ pinMode(batteryPin, INPUT);
+ #endif
+ #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
+ pinMode(internalLedPin, OUTPUT);
+ #endif
+ if (Config.externalVoltageMeasurement) {
+ pinMode(Config.externalVoltagePin, INPUT);
+ }
+ #if defined(TTGO_T_Beam_V1_0) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2) || defined(TTGO_T_Beam_V1_2_SX1262)
+ POWER_Utils::setup();
+ #endif
+ delay(1000);
+ Utils::setupDisplay();
+ WIFI_Utils::setup();
+ LoRa_Utils::setup();
+ Utils::validateDigiFreqs();
+ iGateBeaconPacket = GPS_Utils::generateBeacon();
+ iGateLoRaBeaconPacket = GPS_Utils::generateiGateLoRaBeacon();
+ SYSLOG_Utils::setup();
+ BME_Utils::setup();
+ WEB_Utils::setup();
}
void loop() {
- if (stationMode==1 || stationMode==2 ) { // iGate (1 Only Rx / 2 Rx+Tx)
- WIFI_Utils::checkWiFi();
- if (!espClient.connected()) {
- APRS_IS_Utils::connect();
+ WEB_Utils::loop();
+
+ WIFI_Utils::checkIfAutoAPShouldPowerOff();
+
+ if (!WiFiConnected) {
+ thirdLine = Utils::getLocalIP();
}
- APRS_IS_Utils::loop();
- } else if (stationMode==3 || stationMode==4) { // DigiRepeater (3 RxFreq=TxFreq / 4 RxFreq!=TxFreq)
- DIGI_Utils::loop();
- } else if (stationMode==5) { // iGate when WiFi and APRS available , DigiRepeater when not (RxFreq=TxFreq)
- Utils::checkWiFiInterval();
- if (WiFi.status() == WL_CONNECTED) { // iGate Mode
- thirdLine = Utils::getLocalIP();
- if (!espClient.connected()) {
- APRS_IS_Utils::connect();
- }
- if (lastStationModeState == 1) {
- iGateBeaconPacket = GPS_Utils::generateBeacon();
- lastStationModeState = 0;
- Utils::startServer();
- }
- APRS_IS_Utils::loop();
- } else { // DigiRepeater Mode
- DIGI_Utils::loop();
+
+ if (stationMode==1 || stationMode==2 ) { // iGate (1 Only Rx / 2 Rx+Tx)
+ WIFI_Utils::checkWiFi();
+ if (!espClient.connected()) {
+ APRS_IS_Utils::connect();
+ }
+ APRS_IS_Utils::loop();
+ } else if (stationMode==3 || stationMode==4) { // DigiRepeater (3 RxFreq=TxFreq / 4 RxFreq!=TxFreq)
+ DIGI_Utils::loop();
+ } else if (stationMode==5) { // iGate when WiFi and APRS available , DigiRepeater when not (RxFreq=TxFreq)
+ Utils::checkWiFiInterval();
+ if (WiFi.status() == WL_CONNECTED) { // iGate Mode
+ thirdLine = Utils::getLocalIP();
+ if (!espClient.connected()) {
+ APRS_IS_Utils::connect();
+ }
+ if (lastStationModeState == 1) {
+ iGateBeaconPacket = GPS_Utils::generateBeacon();
+ lastStationModeState = 0;
+ }
+ APRS_IS_Utils::loop();
+ } else { // DigiRepeater Mode
+ DIGI_Utils::loop();
+ }
}
- }
}
\ No newline at end of file
diff --git a/src/aprs_is_utils.cpp b/src/aprs_is_utils.cpp
index c672f1b..23e506e 100644
--- a/src/aprs_is_utils.cpp
+++ b/src/aprs_is_utils.cpp
@@ -26,213 +26,214 @@ extern String seventhLine;
namespace APRS_IS_Utils {
- void upload(String line) {
- espClient.print(line + "\r\n");
- }
-
- void connect(){
- int count = 0;
- String aprsauth;
- Serial.print("Connecting to APRS-IS ... ");
- while (!espClient.connect(Config.aprs_is.server.c_str(), Config.aprs_is.port) && count < 20) {
- Serial.println("Didn't connect with server...");
- delay(1000);
- espClient.stop();
- espClient.flush();
- Serial.println("Run client.stop");
- Serial.println("Trying to connect with Server: " + String(Config.aprs_is.server) + " AprsServerPort: " + String(Config.aprs_is.port));
- count++;
- Serial.println("Try: " + String(count));
+ void upload(String line) {
+ espClient.print(line + "\r\n");
}
- if (count == 20) {
- Serial.println("Tried: " + String(count) + " FAILED!");
- } else {
- Serial.println("Connected!\n(Server: " + String(Config.aprs_is.server) + " / Port: " + String(Config.aprs_is.port) +")");
- aprsauth = "user " + Config.callsign + " pass " + Config.aprs_is.passcode + " vers CA2RXU_LoRa_iGate 1.2 filter t/m/" + Config.callsign + "/" + (String)Config.aprs_is.reportingDistance;// + "\r\n";
- upload(aprsauth);
- delay(200);
- }
- }
- void checkStatus() {
- String wifiState, aprsisState;
- if (WiFi.status() == WL_CONNECTED) {
- wifiState = "OK";
- } else {
- wifiState = "--";
- if (!Config.display.alwaysOn) {
- display_toggle(true);
- }
- lastScreenOn = millis();
- }
- if (espClient.connected()) {
- aprsisState = "OK";
- } else {
- aprsisState = "--";
- if (!Config.display.alwaysOn) {
- display_toggle(true);
- }
- lastScreenOn = millis();
- }
- secondLine = "WiFi: " + wifiState + "/ APRS-IS: " + aprsisState;
- }
-
- String createPacket(String packet) {
- if (stationMode>1) {
- return packet.substring(3, packet.indexOf(":")) + ",qAR," + Config.callsign + packet.substring(packet.indexOf(":"));// + "\n";
- } else {
- return packet.substring(3, packet.indexOf(":")) + ",qAO," + Config.callsign + packet.substring(packet.indexOf(":"));// + "\n";
- }
- }
-
- void processLoRaPacket(String packet) {
- bool queryMessage = false;
- String aprsPacket, Sender, AddresseeAndMessage, Addressee, ackMessage, receivedMessage;
- if (packet != "") {
- #ifdef TextSerialOutputForApp
- Serial.println(packet.substring(3));
- #else
- Serial.print("Received Lora Packet : " + String(packet));
- #endif
- if ((packet.substring(0,3) == "\x3c\xff\x01") && (packet.indexOf("TCPIP") == -1) && (packet.indexOf("NOGATE") == -1) && (packet.indexOf("RFONLY") == -1)) {
- #ifndef TextSerialOutputForApp
- Serial.print(" ---> APRS LoRa Packet!");
- #endif
- Sender = packet.substring(3,packet.indexOf(">"));
- if (Sender != Config.callsign) { // avoid listening yourself by digirepeating
- AddresseeAndMessage = packet.substring(packet.indexOf("::")+2);
- Addressee = AddresseeAndMessage.substring(0,AddresseeAndMessage.indexOf(":"));
- Addressee.trim();
-
- if (stationMode!=1 && Config.igateRepeatsLoRaPackets && Addressee != Config.callsign) { // if its not for me
- String digiRepeatedPacket = DIGI_Utils::generateDigiRepeatedPacket(packet.substring(3), Config.callsign);
- if (digiRepeatedPacket != "X") {
- delay(500);
- LoRa_Utils::sendNewPacket("APRS", digiRepeatedPacket);
- }
- }
-
- if (packet.indexOf("::") > 10 && Addressee == Config.callsign) { // its a message for me!
- if (AddresseeAndMessage.indexOf("{")>0) { // ack?
- ackMessage = "ack" + AddresseeAndMessage.substring(AddresseeAndMessage.indexOf("{")+1);
- ackMessage.trim();
- delay(4000);
- //Serial.println(ackMessage);
- for(int i = Sender.length(); i < 9; i++) {
- Sender += ' ';
- }
- LoRa_Utils::sendNewPacket("APRS", Config.callsign + ">APLRG1,RFONLY,WIDE1-1::" + Sender + ":" + ackMessage);
- receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1, AddresseeAndMessage.indexOf("{"));
- } else {
- receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1);
- }
- if (receivedMessage.indexOf("?") == 0) {
- queryMessage = true;
- delay(2000);
- if (!Config.display.alwaysOn) {
- display_toggle(true);
- }
- LoRa_Utils::sendNewPacket("APRS", QUERY_Utils::process(receivedMessage, Sender, "LoRa"));
- lastScreenOn = millis();
- show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, "Callsign = " + Sender, "TYPE --> QUERY", 0);
- }
- }
- if (!queryMessage) {
- aprsPacket = createPacket(packet);
- if (!Config.display.alwaysOn) {
- display_toggle(true);
- }
- lastScreenOn = millis();
- upload(aprsPacket);
- #ifndef TextSerialOutputForApp
- Serial.println(" ---> Uploaded to APRS-IS");
- #endif
- STATION_Utils::updateLastHeard(Sender);
- Utils::typeOfPacket(aprsPacket, "LoRa-APRS");
- show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
- }
- }
- } else {
- #ifndef TextSerialOutputForApp
- Serial.println(" ---> LoRa Packet Ignored (first 3 bytes or TCPIP/NOGATE/RFONLY)\n");
- #endif
- }
- }
- }
-
- void processAPRSISPacket(String packet) {
- String Sender, AddresseeAndMessage, Addressee, receivedMessage;
- if (!packet.startsWith("#")){
- if (packet.indexOf("::")>0) {
- Sender = packet.substring(0,packet.indexOf(">"));
- AddresseeAndMessage = packet.substring(packet.indexOf("::")+2);
- Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
- Addressee.trim();
- if (Addressee == Config.callsign) { // its for me!
- if (AddresseeAndMessage.indexOf("{")>0) { // ack?
- String ackMessage = "ack" + AddresseeAndMessage.substring(AddresseeAndMessage.indexOf("{")+1);
- ackMessage.trim();
- delay(4000);
- //Serial.println(ackMessage);
- for(int i = Sender.length(); i < 9; i++) {
- Sender += ' ';
- }
- String ackPacket = Config.callsign + ">APLRG1,TCPIP,qAC::" + Sender + ":" + ackMessage;// + "\n";
- upload(ackPacket);
- receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1, AddresseeAndMessage.indexOf("{"));
- } else {
- receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1);
- }
- if (receivedMessage.indexOf("?") == 0) {
- #ifndef TextSerialOutputForApp
- Serial.println("Received Query APRS-IS : " + packet);
- #endif
- String queryAnswer = QUERY_Utils::process(receivedMessage, Sender, "APRSIS");
- //Serial.println("---> QUERY Answer : " + queryAnswer.substring(0,queryAnswer.indexOf("\n")));
- if (!Config.display.alwaysOn) {
- display_toggle(true);
- }
- lastScreenOn = millis();
- delay(500);
- upload(queryAnswer);
- SYSLOG_Utils::log("APRSIS Tx", queryAnswer,0,0,0);
- fifthLine = "APRS-IS ----> APRS-IS";
- sixthLine = Config.callsign;
- for (int j=sixthLine.length();j<9;j++) {
- sixthLine += " ";
- }
- sixthLine += "> " + Sender;
- seventhLine = "QUERY = " + receivedMessage;
- }
- } else {
- #ifndef TextSerialOutputForApp
- Serial.print("Received from APRS-IS : " + packet);
- #endif
- if ((stationMode==2 || stationMode==5) && STATION_Utils::wasHeard(Addressee)) {
- LoRa_Utils::sendNewPacket("APRS", LoRa_Utils::generatePacket(packet));
- display_toggle(true);
- lastScreenOn = millis();
- Utils::typeOfPacket(packet, "APRS-LoRa");
- }
+ void connect(){
+ int count = 0;
+ String aprsauth;
+ Serial.print("Connecting to APRS-IS ... ");
+ while (!espClient.connect(Config.aprs_is.server.c_str(), Config.aprs_is.port) && count < 20) {
+ Serial.println("Didn't connect with server...");
+ delay(1000);
+ espClient.stop();
+ espClient.flush();
+ Serial.println("Run client.stop");
+ Serial.println("Trying to connect with Server: " + String(Config.aprs_is.server) + " AprsServerPort: " + String(Config.aprs_is.port));
+ count++;
+ Serial.println("Try: " + String(count));
+ }
+ if (count == 20) {
+ Serial.println("Tried: " + String(count) + " FAILED!");
+ } else {
+ Serial.println("Connected!\n(Server: " + String(Config.aprs_is.server) + " / Port: " + String(Config.aprs_is.port) +")");
+ aprsauth = "user " + Config.callsign + " pass " + Config.aprs_is.passcode + " vers CA2RXU_LoRa_iGate 1.3 filter t/m/" + Config.callsign + "/" + (String)Config.aprs_is.reportingDistance;// + "\r\n";
+ upload(aprsauth);
+ delay(200);
}
- show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
- }
}
- }
- void loop() {
- checkStatus();
- show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
- while (espClient.connected()) {
- Utils::checkDisplayInterval();
- Utils::checkBeaconInterval();
- processLoRaPacket(LoRa_Utils::receivePacket());
- if (espClient.available()) {
- String aprsisPacket;
- aprsisPacket.concat(espClient.readStringUntil('\r'));
- processAPRSISPacket(aprsisPacket);
- }
- ElegantOTA.loop();
+ void checkStatus() {
+ String wifiState, aprsisState;
+ if (WiFi.status() == WL_CONNECTED) {
+ wifiState = "OK";
+ } else {
+ wifiState = "AP";
+ if (!Config.display.alwaysOn) {
+ display_toggle(true);
+ }
+ lastScreenOn = millis();
+ }
+ if (espClient.connected()) {
+ aprsisState = "OK";
+ } else {
+ aprsisState = "--";
+ if (!Config.display.alwaysOn) {
+ display_toggle(true);
+ }
+ lastScreenOn = millis();
+ }
+ secondLine = "WiFi: " + wifiState + "/ APRS-IS: " + aprsisState;
}
- }
+
+ String createPacket(String packet) {
+ if (stationMode>1) {
+ return packet.substring(3, packet.indexOf(":")) + ",qAR," + Config.callsign + packet.substring(packet.indexOf(":"));// + "\n";
+ } else {
+ return packet.substring(3, packet.indexOf(":")) + ",qAO," + Config.callsign + packet.substring(packet.indexOf(":"));// + "\n";
+ }
+ }
+
+ void processLoRaPacket(String packet) {
+ bool queryMessage = false;
+ String aprsPacket, Sender, AddresseeAndMessage, Addressee, ackMessage, receivedMessage;
+ if (packet != "") {
+ #ifdef TextSerialOutputForApp
+ Serial.println(packet.substring(3));
+ #else
+ Serial.print("Received Lora Packet : " + String(packet));
+ #endif
+ if ((packet.substring(0,3) == "\x3c\xff\x01") && (packet.indexOf("TCPIP") == -1) && (packet.indexOf("NOGATE") == -1) && (packet.indexOf("RFONLY") == -1)) {
+ #ifndef TextSerialOutputForApp
+ Serial.print(" ---> APRS LoRa Packet!");
+ #endif
+ Sender = packet.substring(3,packet.indexOf(">"));
+ if (Sender != Config.callsign) { // avoid listening yourself by digirepeating
+ AddresseeAndMessage = packet.substring(packet.indexOf("::")+2);
+ Addressee = AddresseeAndMessage.substring(0,AddresseeAndMessage.indexOf(":"));
+ Addressee.trim();
+
+ if (stationMode!=1 && Config.igateRepeatsLoRaPackets && Addressee != Config.callsign) { // if its not for me
+ String digiRepeatedPacket = DIGI_Utils::generateDigiRepeatedPacket(packet.substring(3), Config.callsign);
+ if (digiRepeatedPacket != "X") {
+ delay(500);
+ LoRa_Utils::sendNewPacket("APRS", digiRepeatedPacket);
+ }
+ }
+
+ if (packet.indexOf("::") > 10 && Addressee == Config.callsign) { // its a message for me!
+ if (AddresseeAndMessage.indexOf("{")>0) { // ack?
+ ackMessage = "ack" + AddresseeAndMessage.substring(AddresseeAndMessage.indexOf("{")+1);
+ ackMessage.trim();
+ delay(4000);
+ //Serial.println(ackMessage);
+ for(int i = Sender.length(); i < 9; i++) {
+ Sender += ' ';
+ }
+ LoRa_Utils::sendNewPacket("APRS", Config.callsign + ">APLRG1,RFONLY,WIDE1-1::" + Sender + ":" + ackMessage);
+ receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1, AddresseeAndMessage.indexOf("{"));
+ } else {
+ receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1);
+ }
+ if (receivedMessage.indexOf("?") == 0) {
+ queryMessage = true;
+ delay(2000);
+ if (!Config.display.alwaysOn) {
+ display_toggle(true);
+ }
+ LoRa_Utils::sendNewPacket("APRS", QUERY_Utils::process(receivedMessage, Sender, "LoRa"));
+ lastScreenOn = millis();
+ show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, "Callsign = " + Sender, "TYPE --> QUERY", 0);
+ }
+ }
+ if (!queryMessage) {
+ aprsPacket = createPacket(packet);
+ if (!Config.display.alwaysOn) {
+ display_toggle(true);
+ }
+ lastScreenOn = millis();
+ upload(aprsPacket);
+ #ifndef TextSerialOutputForApp
+ Serial.println(" ---> Uploaded to APRS-IS");
+ #endif
+ STATION_Utils::updateLastHeard(Sender);
+ Utils::typeOfPacket(aprsPacket, "LoRa-APRS");
+ show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
+ }
+ }
+ } else {
+ #ifndef TextSerialOutputForApp
+ Serial.println(" ---> LoRa Packet Ignored (first 3 bytes or TCPIP/NOGATE/RFONLY)\n");
+ #endif
+ }
+ }
+ }
+
+ void processAPRSISPacket(String packet) {
+ String Sender, AddresseeAndMessage, Addressee, receivedMessage;
+ if (!packet.startsWith("#")){
+ if (packet.indexOf("::")>0) {
+ Sender = packet.substring(0,packet.indexOf(">"));
+ AddresseeAndMessage = packet.substring(packet.indexOf("::")+2);
+ Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
+ Addressee.trim();
+ if (Addressee == Config.callsign) { // its for me!
+ if (AddresseeAndMessage.indexOf("{")>0) { // ack?
+ String ackMessage = "ack" + AddresseeAndMessage.substring(AddresseeAndMessage.indexOf("{")+1);
+ ackMessage.trim();
+ delay(4000);
+ //Serial.println(ackMessage);
+ for(int i = Sender.length(); i < 9; i++) {
+ Sender += ' ';
+ }
+ String ackPacket = Config.callsign + ">APLRG1,TCPIP,qAC::" + Sender + ":" + ackMessage;// + "\n";
+ upload(ackPacket);
+ receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1, AddresseeAndMessage.indexOf("{"));
+ } else {
+ receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1);
+ }
+ if (receivedMessage.indexOf("?") == 0) {
+ #ifndef TextSerialOutputForApp
+ Serial.println("Received Query APRS-IS : " + packet);
+ #endif
+ String queryAnswer = QUERY_Utils::process(receivedMessage, Sender, "APRSIS");
+ //Serial.println("---> QUERY Answer : " + queryAnswer.substring(0,queryAnswer.indexOf("\n")));
+ if (!Config.display.alwaysOn) {
+ display_toggle(true);
+ }
+ lastScreenOn = millis();
+ delay(500);
+ upload(queryAnswer);
+ SYSLOG_Utils::log("APRSIS Tx", queryAnswer,0,0,0);
+ fifthLine = "APRS-IS ----> APRS-IS";
+ sixthLine = Config.callsign;
+ for (int j=sixthLine.length();j<9;j++) {
+ sixthLine += " ";
+ }
+ sixthLine += "> " + Sender;
+ seventhLine = "QUERY = " + receivedMessage;
+ }
+ } else {
+ #ifndef TextSerialOutputForApp
+ Serial.print("Received from APRS-IS : " + packet);
+ #endif
+ if ((stationMode==2 || stationMode==5) && STATION_Utils::wasHeard(Addressee)) {
+ LoRa_Utils::sendNewPacket("APRS", LoRa_Utils::generatePacket(packet));
+ display_toggle(true);
+ lastScreenOn = millis();
+ Utils::typeOfPacket(packet, "APRS-LoRa");
+ }
+ }
+ show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
+ }
+ }
+ }
+
+ void loop() {
+ checkStatus();
+ show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
+ while (espClient.connected()) {
+ Utils::checkDisplayInterval();
+ Utils::checkBeaconInterval();
+ processLoRaPacket(LoRa_Utils::receivePacket());
+ if (espClient.available()) {
+ String aprsisPacket;
+ aprsisPacket.concat(espClient.readStringUntil('\r'));
+ processAPRSISPacket(aprsisPacket);
+ }
+ ElegantOTA.loop();
+ }
+ }
+
}
\ No newline at end of file
diff --git a/src/bme_utils.cpp b/src/bme_utils.cpp
index 376562d..6547c08 100644
--- a/src/bme_utils.cpp
+++ b/src/bme_utils.cpp
@@ -13,144 +13,144 @@ extern String fifthLine;
namespace BME_Utils {
- #ifdef BME280Sensor
- Adafruit_BME280 bme;
- #endif
- #ifdef BMP280Sensor
- Adafruit_BMP280 bme;
- #endif
- #ifdef BME680Sensor
- Adafruit_BME680 bme;
- #endif
-
- void setup() {
- if (Config.bme.active) {
- bool status;
- status = bme.begin(0x76); // Don't forget to join pins for righ direction on BME280!
- if (!status) {
- Serial.println("Could not find a valid BME280 or BMP280 sensor, check wiring!");
- show_display("ERROR", "", "BME/BMP sensor active", "but no sensor found...");
- while (1); // sacar esto para que quede pegado si no encuentra BME280
- } else {
- #ifdef BME280Sensor
- Serial.println("init : BME280 Module ... done!");
- #endif
- #ifdef BMP280Sensor
- Serial.println("init : BMP280 Module ... done!");
- #endif
- #ifdef BME680Sensor
- Serial.println("init : BME680 Module ... done!");
- #endif
- }
- } else {
- Serial.println("(BME/BMP sensor not 'active' in 'igate_conf.json')");
- }
- }
-
- String generateTempString(float bmeTemp) {
- String strTemp;
- strTemp = String((int)bmeTemp);
- switch (strTemp.length()) {
- case 1:
- return "00" + strTemp;
- break;
- case 2:
- return "0" + strTemp;
- break;
- case 3:
- return strTemp;
- break;
- default:
- return "-999";
- }
- }
-
- String generateHumString(float bmeHum) {
- String strHum;
- strHum = String((int)bmeHum);
- switch (strHum.length()) {
- case 1:
- return "0" + strHum;
- break;
- case 2:
- return strHum;
- break;
- case 3:
- if ((int)bmeHum == 100) {
- return "00";
- } else {
- return "-99";
- }
- break;
- default:
- return "-99";
- }
- }
-
- String generatePresString(float bmePress) {
- String strPress;
- strPress = String((int)bmePress);
- switch (strPress.length()) {
- case 1:
- return "000" + strPress + "0";
- break;
- case 2:
- return "00" + strPress + "0";
- break;
- case 3:
- return "0" + strPress + "0";
- break;
- case 4:
- return strPress + "0";
- break;
- case 5:
- return strPress;
- break;
- default:
- return "-99999";
- }
- }
-
- String readDataSensor() {
- String wx, tempStr, humStr, presStr;
- float newHum;
-
- float newTemp = bme.readTemperature();
- #if defined(BME280Sensor) || defined(BME680Sensor)
- newHum = bme.readHumidity();
+ #ifdef BME280Sensor
+ Adafruit_BME280 bme;
#endif
#ifdef BMP280Sensor
- newHum = 0;
+ Adafruit_BMP280 bme;
#endif
- float newPress = (bme.readPressure() / 100.0F);
-
#ifdef BME680Sensor
- float newGas = bme.gas_resistance / 1000.0; // in Kilo ohms
+ Adafruit_BME680 bme;
#endif
-
- //bme.readAltitude(SEALEVELPRESSURE_HPA) // this is for approximate Altitude Calculation.
-
- if (isnan(newTemp) || isnan(newHum) || isnan(newPress)) {
- Serial.println("BME/BMP Module data failed");
- wx = ".../...g...t...r...p...P...h..b.....";
- fifthLine = "";
- return wx;
- } else {
- tempStr = generateTempString((newTemp * 1.8) + 32);
- #if defined(BME280Sensor) || defined(BME680Sensor)
- humStr = generateHumString(newHum);
- #endif
- #ifdef BMP280Sensor
- humStr = "..";
- #endif
- presStr = generatePresString(newPress + (HEIGHT_CORRECTION/CORRECTION_FACTOR));
- fifthLine = "BME-> " + String(int(newTemp))+"C " + humStr + "% " + presStr.substring(0,4) + "hPa";
- wx = ".../...g...t" + tempStr + "r...p...P...h" + humStr + "b" + presStr;
- #ifdef BME680Sensor
- wx += "Gas: " + String(newGas) + "Kohms ";
- #endif
- return wx;
+
+ void setup() {
+ if (Config.bme.active) {
+ bool status;
+ status = bme.begin(0x76); // Don't forget to join pins for righ direction on BME280!
+ if (!status) {
+ Serial.println("Could not find a valid BME280 or BMP280 sensor, check wiring!");
+ show_display("ERROR", "", "BME/BMP sensor active", "but no sensor found...");
+ while (1); // sacar esto para que quede pegado si no encuentra BME280
+ } else {
+ #ifdef BME280Sensor
+ Serial.println("init : BME280 Module ... done!");
+ #endif
+ #ifdef BMP280Sensor
+ Serial.println("init : BMP280 Module ... done!");
+ #endif
+ #ifdef BME680Sensor
+ Serial.println("init : BME680 Module ... done!");
+ #endif
+ }
+ } else {
+ Serial.println("(BME/BMP sensor not 'active' in 'igate_conf.json')");
+ }
+ }
+
+ String generateTempString(float bmeTemp) {
+ String strTemp;
+ strTemp = String((int)bmeTemp);
+ switch (strTemp.length()) {
+ case 1:
+ return "00" + strTemp;
+ break;
+ case 2:
+ return "0" + strTemp;
+ break;
+ case 3:
+ return strTemp;
+ break;
+ default:
+ return "-999";
+ }
+ }
+
+ String generateHumString(float bmeHum) {
+ String strHum;
+ strHum = String((int)bmeHum);
+ switch (strHum.length()) {
+ case 1:
+ return "0" + strHum;
+ break;
+ case 2:
+ return strHum;
+ break;
+ case 3:
+ if ((int)bmeHum == 100) {
+ return "00";
+ } else {
+ return "-99";
+ }
+ break;
+ default:
+ return "-99";
+ }
+ }
+
+ String generatePresString(float bmePress) {
+ String strPress;
+ strPress = String((int)bmePress);
+ switch (strPress.length()) {
+ case 1:
+ return "000" + strPress + "0";
+ break;
+ case 2:
+ return "00" + strPress + "0";
+ break;
+ case 3:
+ return "0" + strPress + "0";
+ break;
+ case 4:
+ return strPress + "0";
+ break;
+ case 5:
+ return strPress;
+ break;
+ default:
+ return "-99999";
+ }
+ }
+
+ String readDataSensor() {
+ String wx, tempStr, humStr, presStr;
+ float newHum;
+
+ float newTemp = bme.readTemperature();
+ #if defined(BME280Sensor) || defined(BME680Sensor)
+ newHum = bme.readHumidity();
+ #endif
+ #ifdef BMP280Sensor
+ newHum = 0;
+ #endif
+ float newPress = (bme.readPressure() / 100.0F);
+
+ #ifdef BME680Sensor
+ float newGas = bme.gas_resistance / 1000.0; // in Kilo ohms
+ #endif
+
+ //bme.readAltitude(SEALEVELPRESSURE_HPA) // this is for approximate Altitude Calculation.
+
+ if (isnan(newTemp) || isnan(newHum) || isnan(newPress)) {
+ Serial.println("BME/BMP Module data failed");
+ wx = ".../...g...t...r...p...P...h..b.....";
+ fifthLine = "";
+ return wx;
+ } else {
+ tempStr = generateTempString((newTemp * 1.8) + 32);
+ #if defined(BME280Sensor) || defined(BME680Sensor)
+ humStr = generateHumString(newHum);
+ #endif
+ #ifdef BMP280Sensor
+ humStr = "..";
+ #endif
+ presStr = generatePresString(newPress + (HEIGHT_CORRECTION/CORRECTION_FACTOR));
+ fifthLine = "BME-> " + String(int(newTemp))+"C " + humStr + "% " + presStr.substring(0,4) + "hPa";
+ wx = ".../...g...t" + tempStr + "r...p...P...h" + humStr + "b" + presStr;
+ #ifdef BME680Sensor
+ wx += "Gas: " + String(newGas) + "Kohms ";
+ #endif
+ return wx;
+ }
}
- }
}
\ No newline at end of file
diff --git a/src/configuration.cpp b/src/configuration.cpp
index a073d34..0d95409 100644
--- a/src/configuration.cpp
+++ b/src/configuration.cpp
@@ -3,84 +3,229 @@
#include "configuration.h"
#include "display.h"
-Configuration::Configuration() {
- _filePath = "/igate_conf.json";
- if (!SPIFFS.begin(false)) {
- Serial.println("SPIFFS Mount Failed");
- return;
- }
- readFile(SPIFFS, _filePath.c_str());
-}
-void Configuration::readFile(fs::FS &fs, const char *fileName) {
+
+void Configuration::writeFile() {
+ Serial.println("Saving config..");
+
StaticJsonDocument<1536> data;
- File configFile = fs.open(fileName, "r");
- DeserializationError error = deserializeJson(data, configFile);
- if (error) {
- Serial.println("Failed to read file, using default configuration");
+ File configFile = SPIFFS.open("/igate_conf.json", "w");
+
+ if (wifiAPs[0].ssid != "") { // We don't want to save Auto AP empty SSID
+ for (int i = 0; i < wifiAPs.size(); i++) {
+ data["wifi"]["AP"][i]["ssid"] = wifiAPs[i].ssid;
+ data["wifi"]["AP"][i]["password"] = wifiAPs[i].password;
+ data["wifi"]["AP"][i]["latitude"] = wifiAPs[i].latitude;
+ data["wifi"]["AP"][i]["longitude"] = wifiAPs[i].longitude;
+ }
}
- JsonArray WiFiArray = data["wifi"]["AP"];
- for (int i = 0; i < WiFiArray.size(); i++) {
- WiFi_AP wifiap;
- wifiap.ssid = WiFiArray[i]["ssid"].as();
- wifiap.password = WiFiArray[i]["password"].as();
- wifiap.latitude = WiFiArray[i]["latitude"].as();
- wifiap.longitude = WiFiArray[i]["longitude"].as();
+ data["wifi"]["autoAP"]["password"] = wifiAutoAP.password;
+ data["wifi"]["autoAP"]["powerOff"] = wifiAutoAP.powerOff;
- wifiAPs.push_back(wifiap);
- }
+ data["callsign"] = callsign;
+ data["stationMode"] = stationMode;
+ data["iGateComment"] = iGateComment;
- callsign = data["callsign"].as();
- stationMode = data["stationMode"].as();
- iGateComment = data["iGateComment"].as();
- beaconInterval = data["other"]["beaconInterval"].as();
- igateSendsLoRaBeacons = data["other"]["igateSendsLoRaBeacons"].as();
- igateRepeatsLoRaPackets = data["other"]["igateRepeatsLoRaPackets"].as();
- rememberStationTime = data["other"]["rememberStationTime"].as();
- sendBatteryVoltage = data["other"]["sendBatteryVoltage"].as();
- externalVoltageMeasurement = data["other"]["externalVoltageMeasurement"].as();
- externalVoltagePin = data["other"]["externalVoltagePin"].as();
+ data["other"]["beaconInterval"] = beaconInterval;
+ data["other"]["igateSendsLoRaBeacons"] = igateSendsLoRaBeacons;
+ data["other"]["igateRepeatsLoRaPackets"] = igateRepeatsLoRaPackets;
+ data["other"]["rememberStationTime"] = rememberStationTime;
+ data["other"]["sendBatteryVoltage"] = sendBatteryVoltage;
+ data["other"]["externalVoltageMeasurement"] = externalVoltageMeasurement;
+ data["other"]["externalVoltagePin"] = externalVoltagePin;
- digi.comment = data["digi"]["comment"].as();
- digi.latitude = data["digi"]["latitude"].as();
- digi.longitude = data["digi"]["longitude"].as();
+ data["digi"]["comment"] = digi.comment;
+ data["digi"]["latitude"] = digi.latitude;
+ data["digi"]["longitude"] = digi.longitude;
- aprs_is.passcode = data["aprs_is"]["passcode"].as();
- aprs_is.server = data["aprs_is"]["server"].as();
- aprs_is.port = data["aprs_is"]["port"].as();
- aprs_is.reportingDistance = data["aprs_is"]["reportingDistance"].as();
+ data["aprs_is"]["passcode"] = aprs_is.passcode;
+ data["aprs_is"]["server"] = aprs_is.server;
+ data["aprs_is"]["port"] = aprs_is.port;
+ data["aprs_is"]["reportingDistance"] = aprs_is.reportingDistance;
- loramodule.iGateFreq = data["lora"]["iGateFreq"].as();
- loramodule.digirepeaterTxFreq = data["lora"]["digirepeaterTxFreq"].as();
- loramodule.digirepeaterRxFreq = data["lora"]["digirepeaterRxFreq"].as();
- loramodule.spreadingFactor = data["lora"]["spreadingFactor"].as();
- loramodule.signalBandwidth = data["lora"]["signalBandwidth"].as();
- loramodule.codingRate4 = data["lora"]["codingRate4"].as();
- loramodule.power = data["lora"]["power"].as();
+ data["lora"]["iGateFreq"] = loramodule.iGateFreq;
+ data["lora"]["digirepeaterTxFreq"] = loramodule.digirepeaterTxFreq;
+ data["lora"]["digirepeaterRxFreq"] = loramodule.digirepeaterRxFreq;
+ data["lora"]["spreadingFactor"] = loramodule.spreadingFactor;
+ data["lora"]["signalBandwidth"] = loramodule.signalBandwidth;
+ data["lora"]["codingRate4"] = loramodule.codingRate4;
+ data["lora"]["power"] = loramodule.power;
- display.alwaysOn = data["display"]["alwaysOn"].as();
- display.timeout = data["display"]["timeout"].as();
- display.turn180 = data["display"]["turn180"].as();
+ data["display"]["alwaysOn"] = display.alwaysOn;
+ data["display"]["timeout"] = display.timeout;
+ data["display"]["turn180"] = display.turn180;
- syslog.active = data["syslog"]["active"].as();
- syslog.server = data["syslog"]["server"].as();
- syslog.port = data["syslog"]["port"].as();
+ data["syslog"]["active"] = syslog.active;
+ data["syslog"]["server"] = syslog.server;
+ data["syslog"]["port"] = syslog.port;
- bme.active = data["bme"]["active"].as();
+ data["bme"]["active"] = bme.active;
- ota.username = data["ota"]["username"].as();
- ota.password = data["ota"]["password"].as();
+ data["ota"]["username"] = ota.username;
+ data["ota"]["password"] = ota.password;
+ serializeJson(data, configFile);
+
configFile.close();
+
+ Serial.println("Config saved");
}
-void Configuration::validateConfigFile(String currentBeaconCallsign) {
- if (currentBeaconCallsign == "NOCALL-10") {
- Serial.println("Change Callsign in /data/igate_conf.json");
- show_display("------- ERROR -------", "Change your settings", "on 'igate_conf.json'", "--> File System image", 0);
- while (true) {
- delay(1000);
+bool Configuration::readFile() {
+ Serial.println("Reading config..");
+
+ File configFile = SPIFFS.open("/igate_conf.json", "r");
+
+ if (configFile) {
+ StaticJsonDocument<1536> data;
+
+ DeserializationError error = deserializeJson(data, configFile);
+ if (error) {
+ Serial.println("Failed to read file, using default configuration");
+ }
+
+ JsonArray WiFiArray = data["wifi"]["AP"];
+ for (int i = 0; i < WiFiArray.size(); i++) {
+ WiFi_AP wifiap;
+ wifiap.ssid = WiFiArray[i]["ssid"].as();
+ wifiap.password = WiFiArray[i]["password"].as();
+ wifiap.latitude = WiFiArray[i]["latitude"].as();
+ wifiap.longitude = WiFiArray[i]["longitude"].as();
+
+ wifiAPs.push_back(wifiap);
+ }
+
+ wifiAutoAP.password = data["wifi"]["autoAP"]["password"].as();
+ wifiAutoAP.powerOff = data["wifi"]["autoAP"]["powerOff"].as();
+
+ callsign = data["callsign"].as();
+ stationMode = data["stationMode"].as();
+ iGateComment = data["iGateComment"].as();
+ beaconInterval = data["other"]["beaconInterval"].as();
+ igateSendsLoRaBeacons = data["other"]["igateSendsLoRaBeacons"].as();
+ igateRepeatsLoRaPackets = data["other"]["igateRepeatsLoRaPackets"].as();
+ rememberStationTime = data["other"]["rememberStationTime"].as();
+ sendBatteryVoltage = data["other"]["sendBatteryVoltage"].as();
+ externalVoltageMeasurement = data["other"]["externalVoltageMeasurement"].as();
+ externalVoltagePin = data["other"]["externalVoltagePin"].as();
+
+ digi.comment = data["digi"]["comment"].as();
+ digi.latitude = data["digi"]["latitude"].as();
+ digi.longitude = data["digi"]["longitude"].as();
+
+ aprs_is.passcode = data["aprs_is"]["passcode"].as();
+ aprs_is.server = data["aprs_is"]["server"].as();
+ aprs_is.port = data["aprs_is"]["port"].as();
+ aprs_is.reportingDistance = data["aprs_is"]["reportingDistance"].as();
+
+ loramodule.iGateFreq = data["lora"]["iGateFreq"].as();
+ loramodule.digirepeaterTxFreq = data["lora"]["digirepeaterTxFreq"].as();
+ loramodule.digirepeaterRxFreq = data["lora"]["digirepeaterRxFreq"].as();
+ loramodule.spreadingFactor = data["lora"]["spreadingFactor"].as();
+ loramodule.signalBandwidth = data["lora"]["signalBandwidth"].as();
+ loramodule.codingRate4 = data["lora"]["codingRate4"].as();
+ loramodule.power = data["lora"]["power"].as();
+
+ display.alwaysOn = data["display"]["alwaysOn"].as();
+ display.timeout = data["display"]["timeout"].as();
+ display.turn180 = data["display"]["turn180"].as();
+
+ syslog.active = data["syslog"]["active"].as();
+ syslog.server = data["syslog"]["server"].as();
+ syslog.port = data["syslog"]["port"].as();
+
+ bme.active = data["bme"]["active"].as();
+
+ ota.username = data["ota"]["username"].as();
+ ota.password = data["ota"]["password"].as();
+
+ if (wifiAPs.size() == 0) { // If we don't have any WiFi's from config we need to add "empty" SSID for AUTO AP
+ WiFi_AP wifiap;
+ wifiap.ssid = "";
+ wifiap.password = "";
+ wifiap.latitude = 0.0;
+ wifiap.longitude = 0.0;
+
+ wifiAPs.push_back(wifiap);
+ }
+ configFile.close();
+ Serial.println("Config read successfuly");
+ return true;
+ } else {
+ Serial.println("Config file not found");
+ return false;
}
- }
+}
+
+void Configuration::init() {
+ WiFi_AP wifiap;
+ wifiap.ssid = "";
+ wifiap.password = "";
+ wifiap.latitude = 0.0;
+ wifiap.longitude = 0.0;
+ wifiAPs.push_back(wifiap);
+
+ wifiAutoAP.password = "1234567890";
+ wifiAutoAP.powerOff = 15;
+
+ callsign = "N0CALL";
+ stationMode = 1;
+ iGateComment = "LoRa_APRS_iGate Development";
+
+ digi.comment = "LoRa_APRS_iGate Development";
+ digi.latitude = 0.0;
+ digi.longitude = 0.0;
+
+ aprs_is.passcode = "XYZVW";
+ aprs_is.server = "rotate.aprs2.net";
+ aprs_is.port = 14580;
+ aprs_is.reportingDistance = 30;
+
+ loramodule.iGateFreq = 433775000;
+ loramodule.digirepeaterTxFreq = 433775000;
+ loramodule.digirepeaterRxFreq = 433900000;
+ loramodule.spreadingFactor = 12;
+ loramodule.signalBandwidth = 125000;
+ loramodule.codingRate4 = 5;
+ loramodule.power = 20;
+
+ display.alwaysOn = true;
+ display.timeout = 4;
+ display.turn180 = false;
+
+ syslog.active = false;
+ syslog.server = "192.168.0.100";
+ syslog.port = 514;
+
+ bme.active = false;
+
+ ota.username = "";
+ ota.password = "";
+
+ beaconInterval = 15;
+ igateSendsLoRaBeacons = false;
+ igateRepeatsLoRaPackets = false;
+ rememberStationTime = 30;
+ sendBatteryVoltage = false;
+ externalVoltageMeasurement = false;
+ externalVoltagePin = 34;
+
+ Serial.println("todo escrito");
+}
+
+Configuration::Configuration() {
+ if (!SPIFFS.begin(false)) {
+ Serial.println("SPIFFS Mount Failed");
+ return;
+ } else {
+ Serial.println("montado");
+ }
+ bool exists = SPIFFS.exists("/igate_conf.json");
+ if (!exists) {
+ init();
+ writeFile();
+ ESP.restart();
+ }
+ readFile();
}
\ No newline at end of file
diff --git a/src/configuration.h b/src/configuration.h
index 91a0405..04ee554 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -7,61 +7,68 @@
class WiFi_AP {
public:
- String ssid;
- String password;
- double latitude;
- double longitude;
+ String ssid;
+ String password;
+ double latitude;
+ double longitude;
};
+class WiFi_Auto_AP {
+public:
+ String password;
+ int powerOff;
+};
+
+
class DIGI {
public:
- String comment;
- double latitude;
- double longitude;
+ String comment;
+ double latitude;
+ double longitude;
};
class APRS_IS {
public:
- String passcode;
- String server;
- int port;
- int reportingDistance;
+ String passcode;
+ String server;
+ int port;
+ int reportingDistance;
};
class LoraModule {
public:
- long iGateFreq;
- long digirepeaterTxFreq;
- long digirepeaterRxFreq;
- int spreadingFactor;
- long signalBandwidth;
- int codingRate4;
- int power;
+ long iGateFreq;
+ long digirepeaterTxFreq;
+ long digirepeaterRxFreq;
+ int spreadingFactor;
+ long signalBandwidth;
+ int codingRate4;
+ int power;
};
class Display {
public:
- bool alwaysOn;
- int timeout;
- bool turn180;
+ bool alwaysOn;
+ int timeout;
+ bool turn180;
};
class SYSLOG {
public:
- bool active;
- String server;
- int port;
+ bool active;
+ String server;
+ int port;
};
class BME {
public:
- bool active;
+ bool active;
};
class OTA {
public:
- String username;
- String password;
+ String username;
+ String password;
};
@@ -69,31 +76,33 @@ public:
class Configuration {
public:
- String callsign;
- int stationMode;
- String iGateComment;
- int beaconInterval;
- bool igateSendsLoRaBeacons;
- bool igateRepeatsLoRaPackets;
- int rememberStationTime;
- bool sendBatteryVoltage;
- bool externalVoltageMeasurement;
- int externalVoltagePin;
- std::vector wifiAPs;
- DIGI digi;
- APRS_IS aprs_is;
- LoraModule loramodule;
- Display display;
- SYSLOG syslog;
- BME bme;
- OTA ota;
+ String callsign;
+ int stationMode;
+ String iGateComment;
+ int beaconInterval;
+ bool igateSendsLoRaBeacons;
+ bool igateRepeatsLoRaPackets;
+ int rememberStationTime;
+ bool sendBatteryVoltage;
+ bool externalVoltageMeasurement;
+ int externalVoltagePin;
+ std::vector wifiAPs;
+ WiFi_Auto_AP wifiAutoAP;
+ DIGI digi;
+ APRS_IS aprs_is;
+ LoraModule loramodule;
+ Display display;
+ SYSLOG syslog;
+ BME bme;
+ OTA ota;
-
- Configuration();
- void validateConfigFile(String currentBeaconCallsign);
+ void init();
+ void writeFile();
+ Configuration();
private:
- void readFile(fs::FS &fs, const char *fileName) ;
- String _filePath;
+ bool readFile();
+ String _filePath;
};
+
#endif
\ No newline at end of file
diff --git a/src/display.cpp b/src/display.cpp
index 8b60471..1857015 100644
--- a/src/display.cpp
+++ b/src/display.cpp
@@ -10,156 +10,156 @@ Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
extern Configuration Config;
void setup_display() {
- Wire.begin(OLED_SDA, OLED_SCL);
+ Wire.begin(OLED_SDA, OLED_SCL);
- if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
- Serial.println(F("SSD1306 allocation failed"));
- for(;;); // Don't proceed, loop forever
- }
- if (Config.display.turn180) {
- display.setRotation(2);
- }
- display.clearDisplay();
- display.setTextColor(WHITE);
- display.setTextSize(1);
- display.setCursor(0, 0);
- display.ssd1306_command(SSD1306_SETCONTRAST);
- display.ssd1306_command(1);
- display.display();
- delay(1000);
+ if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
+ Serial.println(F("SSD1306 allocation failed"));
+ for(;;); // Don't proceed, loop forever
+ }
+ if (Config.display.turn180) {
+ display.setRotation(2);
+ }
+ display.clearDisplay();
+ display.setTextColor(WHITE);
+ display.setTextSize(1);
+ display.setCursor(0, 0);
+ display.ssd1306_command(SSD1306_SETCONTRAST);
+ display.ssd1306_command(1);
+ display.display();
+ delay(1000);
}
void display_toggle(bool toggle) {
- if (toggle) {
- display.ssd1306_command(SSD1306_DISPLAYON);
- } else {
- display.ssd1306_command(SSD1306_DISPLAYOFF);
- }
+ if (toggle) {
+ display.ssd1306_command(SSD1306_DISPLAYON);
+ } else {
+ display.ssd1306_command(SSD1306_DISPLAYOFF);
+ }
}
void show_display(String line1, int wait) {
- display.clearDisplay();
- display.setTextColor(WHITE);
- display.setTextSize(1);
- display.setCursor(0, 0);
- display.println(line1);
- display.ssd1306_command(SSD1306_SETCONTRAST);
- display.ssd1306_command(1);
- display.display();
- delay(wait);
+ display.clearDisplay();
+ display.setTextColor(WHITE);
+ display.setTextSize(1);
+ display.setCursor(0, 0);
+ display.println(line1);
+ display.ssd1306_command(SSD1306_SETCONTRAST);
+ display.ssd1306_command(1);
+ display.display();
+ delay(wait);
}
void show_display(String line1, String line2, int wait) {
- display.clearDisplay();
- display.setTextColor(WHITE);
- display.setTextSize(1);
- display.setCursor(0, 0);
- display.println(line1);
- display.setCursor(0, 8);
- display.println(line2);
- display.ssd1306_command(SSD1306_SETCONTRAST);
- display.ssd1306_command(1);
- display.display();
- delay(wait);
+ display.clearDisplay();
+ display.setTextColor(WHITE);
+ display.setTextSize(1);
+ display.setCursor(0, 0);
+ display.println(line1);
+ display.setCursor(0, 8);
+ display.println(line2);
+ display.ssd1306_command(SSD1306_SETCONTRAST);
+ display.ssd1306_command(1);
+ display.display();
+ delay(wait);
}
void show_display(String line1, String line2, String line3, int wait) {
- display.clearDisplay();
- display.setTextColor(WHITE);
- display.setTextSize(1);
- display.setCursor(0, 0);
- display.println(line1);
- display.setCursor(0, 8);
- display.println(line2);
- display.setCursor(0, 16);
- display.println(line3);
- display.ssd1306_command(SSD1306_SETCONTRAST);
- display.ssd1306_command(1);
- display.display();
- delay(wait);
+ display.clearDisplay();
+ display.setTextColor(WHITE);
+ display.setTextSize(1);
+ display.setCursor(0, 0);
+ display.println(line1);
+ display.setCursor(0, 8);
+ display.println(line2);
+ display.setCursor(0, 16);
+ display.println(line3);
+ display.ssd1306_command(SSD1306_SETCONTRAST);
+ display.ssd1306_command(1);
+ display.display();
+ delay(wait);
}
void show_display(String line1, String line2, String line3, String line4, int wait) {
- display.clearDisplay();
- display.setTextColor(WHITE);
- display.setTextSize(1);
- display.setCursor(0, 0);
- display.println(line1);
- display.setCursor(0, 8);
- display.println(line2);
- display.setCursor(0, 16);
- display.println(line3);
- display.setCursor(0, 24);
- display.println(line4);
- display.ssd1306_command(SSD1306_SETCONTRAST);
- display.ssd1306_command(1);
- display.display();
- delay(wait);
+ display.clearDisplay();
+ display.setTextColor(WHITE);
+ display.setTextSize(1);
+ display.setCursor(0, 0);
+ display.println(line1);
+ display.setCursor(0, 8);
+ display.println(line2);
+ display.setCursor(0, 16);
+ display.println(line3);
+ display.setCursor(0, 24);
+ display.println(line4);
+ display.ssd1306_command(SSD1306_SETCONTRAST);
+ display.ssd1306_command(1);
+ display.display();
+ delay(wait);
}
void show_display(String line1, String line2, String line3, String line4, String line5, int wait) {
- display.clearDisplay();
- display.setTextColor(WHITE);
- display.setTextSize(1);
- display.setCursor(0, 0);
- display.println(line1);
- display.setCursor(0, 8);
- display.println(line2);
- display.setCursor(0, 16);
- display.println(line3);
- display.setCursor(0, 24);
- display.println(line4);
- display.setCursor(0, 32);
- display.println(line5);
- display.ssd1306_command(SSD1306_SETCONTRAST);
- display.ssd1306_command(1);
- display.display();
- delay(wait);
+ display.clearDisplay();
+ display.setTextColor(WHITE);
+ display.setTextSize(1);
+ display.setCursor(0, 0);
+ display.println(line1);
+ display.setCursor(0, 8);
+ display.println(line2);
+ display.setCursor(0, 16);
+ display.println(line3);
+ display.setCursor(0, 24);
+ display.println(line4);
+ display.setCursor(0, 32);
+ display.println(line5);
+ display.ssd1306_command(SSD1306_SETCONTRAST);
+ display.ssd1306_command(1);
+ display.display();
+ delay(wait);
}
void show_display(String line1, String line2, String line3, String line4, String line5, String line6, int wait) {
- display.clearDisplay();
- display.setTextColor(WHITE);
- display.setTextSize(1);
- display.setCursor(0, 0);
- display.println(line1);
- display.setCursor(0, 8);
- display.println(line2);
- display.setCursor(0, 16);
- display.println(line3);
- display.setCursor(0, 24);
- display.println(line4);
- display.setCursor(0, 32);
- display.println(line5);
- display.setCursor(0, 40);
- display.println(line6);
- display.ssd1306_command(SSD1306_SETCONTRAST);
- display.ssd1306_command(1);
- display.display();
- delay(wait);
+ display.clearDisplay();
+ display.setTextColor(WHITE);
+ display.setTextSize(1);
+ display.setCursor(0, 0);
+ display.println(line1);
+ display.setCursor(0, 8);
+ display.println(line2);
+ display.setCursor(0, 16);
+ display.println(line3);
+ display.setCursor(0, 24);
+ display.println(line4);
+ display.setCursor(0, 32);
+ display.println(line5);
+ display.setCursor(0, 40);
+ display.println(line6);
+ display.ssd1306_command(SSD1306_SETCONTRAST);
+ display.ssd1306_command(1);
+ display.display();
+ delay(wait);
}
void show_display(String line1, String line2, String line3, String line4, String line5, String line6, String line7, int wait) {
- display.clearDisplay();
- display.setTextColor(WHITE);
- display.setTextSize(2);
- display.setCursor(0, 0);
- display.println(line1);
- display.setTextSize(1);
- display.setCursor(0, 16);
- display.println(line2);
- display.setCursor(0, 24);
- display.println(line3);
- display.setCursor(0, 32);
- display.println(line4);
- display.setCursor(0, 40);
- display.println(line5);
- display.setCursor(0, 48);
- display.println(line6);
- display.setCursor(0, 56);
- display.println(line7);
- display.ssd1306_command(SSD1306_SETCONTRAST);
- display.ssd1306_command(1);
- display.display();
- delay(wait);
+ display.clearDisplay();
+ display.setTextColor(WHITE);
+ display.setTextSize(2);
+ display.setCursor(0, 0);
+ display.println(line1);
+ display.setTextSize(1);
+ display.setCursor(0, 16);
+ display.println(line2);
+ display.setCursor(0, 24);
+ display.println(line3);
+ display.setCursor(0, 32);
+ display.println(line4);
+ display.setCursor(0, 40);
+ display.println(line5);
+ display.setCursor(0, 48);
+ display.println(line6);
+ display.setCursor(0, 56);
+ display.println(line7);
+ display.ssd1306_command(SSD1306_SETCONTRAST);
+ display.ssd1306_command(1);
+ display.display();
+ delay(wait);
}
\ No newline at end of file
diff --git a/src/gps_utils.cpp b/src/gps_utils.cpp
index c868da7..1c7916b 100644
--- a/src/gps_utils.cpp
+++ b/src/gps_utils.cpp
@@ -11,188 +11,188 @@ String distance;
namespace GPS_Utils {
- String double2string(double n, int ndec) {
- String r = "";
- if (n>-1 && n<0) {
- r = "-";
- }
- int v = n;
- r += v;
- r += '.';
- for (int i=0;i-1 && n<0) {
+ r = "-";
+ }
+ int v = n;
+ r += v;
+ r += '.';
+ for (int i=0;ilatitude);
- stationLongitude = processLongitudeAPRS(currentWiFi->longitude);
- beaconPacket = Config.callsign + ">APLRG1,WIDE1-1";
- if (stationMode!=6) {
- beaconPacket += ",qAC";
- }
- beaconPacket += ":=" + stationLatitude + "L" + stationLongitude;
- if (stationMode==1) {
- beaconPacket += "&";
- } else {
- beaconPacket += "a";
- }
- beaconPacket += Config.iGateComment;
- } else { //stationMode 3, 4 and 5
- if (stationMode==5) {
+ String generateBeacon() {
+ String stationLatitude, stationLongitude, beaconPacket;
+ if (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status()==WL_CONNECTED && espClient.connected())) {
+ stationLatitude = processLatitudeAPRS(currentWiFi->latitude);
+ stationLongitude = processLongitudeAPRS(currentWiFi->longitude);
+ beaconPacket = Config.callsign + ">APLRG1,WIDE1-1";
+ if (stationMode!=6) {
+ beaconPacket += ",qAC";
+ }
+ beaconPacket += ":=" + stationLatitude + "L" + stationLongitude;
+ if (stationMode==1) {
+ beaconPacket += "&";
+ } else {
+ beaconPacket += "a";
+ }
+ beaconPacket += Config.iGateComment;
+ } else { //stationMode 3, 4 and 5
+ if (stationMode==5) {
+ stationLatitude = processLatitudeAPRS(currentWiFi->latitude);
+ stationLongitude = processLongitudeAPRS(currentWiFi->longitude);
+ } else {
+ stationLatitude = processLatitudeAPRS(Config.digi.latitude);
+ stationLongitude = processLongitudeAPRS(Config.digi.longitude);
+ }
+ beaconPacket = Config.callsign + ">APLRG1,WIDE1-1:=" + stationLatitude + "L" + stationLongitude + "#" + Config.digi.comment;
+ }
+ return beaconPacket;
+ }
+
+ String generateiGateLoRaBeacon() {
+ String stationLatitude, stationLongitude, beaconPacket;
stationLatitude = processLatitudeAPRS(currentWiFi->latitude);
stationLongitude = processLongitudeAPRS(currentWiFi->longitude);
- } else {
- stationLatitude = processLatitudeAPRS(Config.digi.latitude);
- stationLongitude = processLongitudeAPRS(Config.digi.longitude);
- }
- beaconPacket = Config.callsign + ">APLRG1,WIDE1-1:=" + stationLatitude + "L" + stationLongitude + "#" + Config.digi.comment;
+ beaconPacket = Config.callsign + ">APLRG1,RFONLY:=" + stationLatitude + "L" + stationLongitude;
+ if (Config.bme.active) {
+ beaconPacket += "_";
+ } else {
+ beaconPacket += "a";
+ }
+ return beaconPacket;
}
- return beaconPacket;
- }
- String generateiGateLoRaBeacon() {
- String stationLatitude, stationLongitude, beaconPacket;
- stationLatitude = processLatitudeAPRS(currentWiFi->latitude);
- stationLongitude = processLongitudeAPRS(currentWiFi->longitude);
- beaconPacket = Config.callsign + ">APLRG1,RFONLY:=" + stationLatitude + "L" + stationLongitude;
- if (Config.bme.active) {
- beaconPacket += "_";
- } else {
- beaconPacket += "a";
+ double calculateDistanceTo(double latitude, double longitude) {
+ return TinyGPSPlus::distanceBetween(currentWiFi->latitude,currentWiFi->longitude, latitude, longitude) / 1000.0;
}
- return beaconPacket;
- }
- double calculateDistanceTo(double latitude, double longitude) {
- return TinyGPSPlus::distanceBetween(currentWiFi->latitude,currentWiFi->longitude, latitude, longitude) / 1000.0;
- }
+ String decodeEncodedGPS(String packet) {
+ String GPSPacket = packet.substring(packet.indexOf(":!")+3);
+ String encodedLatitude = GPSPacket.substring(0,4);
+ String encodedLongtitude = GPSPacket.substring(4,8);
- String decodeEncodedGPS(String packet) {
- String GPSPacket = packet.substring(packet.indexOf(":!")+3);
- String encodedLatitude = GPSPacket.substring(0,4);
- String encodedLongtitude = GPSPacket.substring(4,8);
-
- int Y1 = int(encodedLatitude[0]);
- int Y2 = int(encodedLatitude[1]);
- int Y3 = int(encodedLatitude[2]);
- int Y4 = int(encodedLatitude[3]);
- float decodedLatitude = 90.0 - ((((Y1-33) * pow(91,3)) + ((Y2-33) * pow(91,2)) + ((Y3-33) * 91) + Y4-33) / 380926.0);
-
- int X1 = int(encodedLongtitude[0]);
- int X2 = int(encodedLongtitude[1]);
- int X3 = int(encodedLongtitude[2]);
- int X4 = int(encodedLongtitude[3]);
- float decodedLongitude = -180.0 + ((((X1-33) * pow(91,3)) + ((X2-33) * pow(91,2)) + ((X3-33) * 91) + X4-33) / 190463.0);
- distance = String(calculateDistanceTo(decodedLatitude, decodedLongitude),1);
- return String(decodedLatitude,5) + "N / " + String(decodedLongitude,5) + "E / " + distance + "km";
- }
-
- String getReceivedGPS(String packet) {
- String infoGPS;
- if (packet.indexOf(":!") > 10) {
- infoGPS = packet.substring(packet.indexOf(":!")+2);
- } else if (packet.indexOf(":=") > 10) {
- infoGPS = packet.substring(packet.indexOf(":=")+2);
+ int Y1 = int(encodedLatitude[0]);
+ int Y2 = int(encodedLatitude[1]);
+ int Y3 = int(encodedLatitude[2]);
+ int Y4 = int(encodedLatitude[3]);
+ float decodedLatitude = 90.0 - ((((Y1-33) * pow(91,3)) + ((Y2-33) * pow(91,2)) + ((Y3-33) * 91) + Y4-33) / 380926.0);
+
+ int X1 = int(encodedLongtitude[0]);
+ int X2 = int(encodedLongtitude[1]);
+ int X3 = int(encodedLongtitude[2]);
+ int X4 = int(encodedLongtitude[3]);
+ float decodedLongitude = -180.0 + ((((X1-33) * pow(91,3)) + ((X2-33) * pow(91,2)) + ((X3-33) * 91) + X4-33) / 190463.0);
+ distance = String(calculateDistanceTo(decodedLatitude, decodedLongitude),1);
+ return String(decodedLatitude,5) + "N / " + String(decodedLongitude,5) + "E / " + distance + "km";
}
- String Latitude = infoGPS.substring(0,8);
- String Longitude = infoGPS.substring(9,18);
- float convertedLatitude, convertedLongitude;
- String firstLatPart = Latitude.substring(0,2);
- String secondLatPart = Latitude.substring(2,4);
- String thirdLatPart = Latitude.substring(Latitude.indexOf(".")+1,Latitude.indexOf(".")+3);
- String firstLngPart = Longitude.substring(0,3);
- String secondLngPart = Longitude.substring(3,5);
- String thirdLngPart = Longitude.substring(Longitude.indexOf(".")+1,Longitude.indexOf(".")+3);
- convertedLatitude = firstLatPart.toFloat() + (secondLatPart.toFloat()/60) + (thirdLatPart.toFloat()/(60*100));
- convertedLongitude = firstLngPart.toFloat() + (secondLngPart.toFloat()/60) + (thirdLngPart.toFloat()/(60*100));
-
- String LatSign = String(Latitude[7]);
- String LngSign = String(Longitude[8]);
- if (LatSign == "S") {
- convertedLatitude = -convertedLatitude;
- }
- if (LngSign == "W") {
- convertedLongitude = -convertedLongitude;
- }
- distance = String(calculateDistanceTo(convertedLatitude, convertedLongitude),1);
- return String(convertedLatitude,5) + "N / " + String(convertedLongitude,5) + "E / " + distance + "km";
- }
+ String getReceivedGPS(String packet) {
+ String infoGPS;
+ if (packet.indexOf(":!") > 10) {
+ infoGPS = packet.substring(packet.indexOf(":!")+2);
+ } else if (packet.indexOf(":=") > 10) {
+ infoGPS = packet.substring(packet.indexOf(":=")+2);
+ }
+ String Latitude = infoGPS.substring(0,8);
+ String Longitude = infoGPS.substring(9,18);
- String getDistance(String packet) {
- int encodedBytePosition = 0;
- if (packet.indexOf(":!") > 10) {
- encodedBytePosition = packet.indexOf(":!") + 14;
+ float convertedLatitude, convertedLongitude;
+ String firstLatPart = Latitude.substring(0,2);
+ String secondLatPart = Latitude.substring(2,4);
+ String thirdLatPart = Latitude.substring(Latitude.indexOf(".")+1,Latitude.indexOf(".")+3);
+ String firstLngPart = Longitude.substring(0,3);
+ String secondLngPart = Longitude.substring(3,5);
+ String thirdLngPart = Longitude.substring(Longitude.indexOf(".")+1,Longitude.indexOf(".")+3);
+ convertedLatitude = firstLatPart.toFloat() + (secondLatPart.toFloat()/60) + (thirdLatPart.toFloat()/(60*100));
+ convertedLongitude = firstLngPart.toFloat() + (secondLngPart.toFloat()/60) + (thirdLngPart.toFloat()/(60*100));
+
+ String LatSign = String(Latitude[7]);
+ String LngSign = String(Longitude[8]);
+ if (LatSign == "S") {
+ convertedLatitude = -convertedLatitude;
+ }
+ if (LngSign == "W") {
+ convertedLongitude = -convertedLongitude;
+ }
+ distance = String(calculateDistanceTo(convertedLatitude, convertedLongitude),1);
+ return String(convertedLatitude,5) + "N / " + String(convertedLongitude,5) + "E / " + distance + "km";
}
- if (packet.indexOf(":=") > 10) {
- encodedBytePosition = packet.indexOf(":=") + 14;
+
+ String getDistance(String packet) {
+ int encodedBytePosition = 0;
+ if (packet.indexOf(":!") > 10) {
+ encodedBytePosition = packet.indexOf(":!") + 14;
+ }
+ if (packet.indexOf(":=") > 10) {
+ encodedBytePosition = packet.indexOf(":=") + 14;
+ }
+ if (encodedBytePosition != 0) {
+ if (String(packet[encodedBytePosition]) == "G" || String(packet[encodedBytePosition]) == "Q" || String(packet[encodedBytePosition]) == "[" || String(packet[encodedBytePosition]) == "H") {
+ return decodeEncodedGPS(packet);
+ } else {
+ return getReceivedGPS(packet);
+ }
+ } else {
+ return " _ / _ / _ ";
+ }
}
- if (encodedBytePosition != 0) {
- if (String(packet[encodedBytePosition]) == "G" || String(packet[encodedBytePosition]) == "Q" || String(packet[encodedBytePosition]) == "[" || String(packet[encodedBytePosition]) == "H") {
- return decodeEncodedGPS(packet);
- } else {
- return getReceivedGPS(packet);
- }
- } else {
- return " _ / _ / _ ";
- }
- }
}
\ No newline at end of file
diff --git a/src/lora_utils.cpp b/src/lora_utils.cpp
index 36e534c..0f91491 100644
--- a/src/lora_utils.cpp
+++ b/src/lora_utils.cpp
@@ -26,197 +26,197 @@ float snr;
namespace LoRa_Utils {
- void setFlag(void) {
- #ifdef HAS_SX126X
- transmissionFlag = true;
- #endif
- }
+ void setFlag(void) {
+ #ifdef HAS_SX126X
+ transmissionFlag = true;
+ #endif
+ }
- void setup() {
- #ifdef HAS_SX127X
- SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
- LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ);
- long freq;
- if (stationMode==1 || stationMode==2) {
- freq = Config.loramodule.iGateFreq;
- } else {
- freq = Config.loramodule.digirepeaterTxFreq;
+ void setup() {
+ #ifdef HAS_SX127X
+ SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
+ LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ);
+ long freq;
+ if (stationMode==1 || stationMode==2) {
+ freq = Config.loramodule.iGateFreq;
+ } else {
+ freq = Config.loramodule.digirepeaterTxFreq;
+ }
+ if (!LoRa.begin(freq)) {
+ Serial.println("Starting LoRa failed!");
+ show_display("ERROR", "Starting LoRa failed!");
+ while (true) {
+ delay(1000);
+ }
+ }
+ LoRa.setSpreadingFactor(Config.loramodule.spreadingFactor);
+ LoRa.setSignalBandwidth(Config.loramodule.signalBandwidth);
+ LoRa.setCodingRate4(Config.loramodule.codingRate4);
+ LoRa.enableCrc();
+ LoRa.setTxPower(Config.loramodule.power);
+ Serial.print("init : LoRa Module ... done!");
+ #endif
+ #ifdef HAS_SX126X
+ SPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN);
+ float freq = (float)Config.loramodule.iGateFreq/1000000;
+ int state = radio.begin(freq);
+ if (state == RADIOLIB_ERR_NONE) {
+ Serial.print("Initializing SX126X LoRa Module");
+ } else {
+ Serial.println("Starting LoRa failed!");
+ while (true);
+ }
+ radio.setDio1Action(setFlag);
+ radio.setSpreadingFactor(Config.loramodule.spreadingFactor);
+ radio.setBandwidth(Config.loramodule.signalBandwidth);
+ radio.setCodingRate(Config.loramodule.codingRate4);
+ #if defined(ESP32_DIY_1W_LoRa)
+ radio.setRfSwitchPins(RADIO_RXEN, RADIO_TXEN);
+ #endif
+ #if defined(HELTEC_V3) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2_SX1262)
+ state = radio.setOutputPower(Config.loramodule.power + 2); // values available: 10, 17, 22 --> if 20 in tracker_conf.json it will be updated to 22.
+ #endif
+ #ifdef ESP32_DIY_1W_LoRa_GPS
+ state = radio.setOutputPower(Config.loramodule.power); // max value 20 (when 20dB in setup 30dB in output as 400M30S has Low Noise Amp)
+ #endif
+ if (state == RADIOLIB_ERR_NONE) {
+ Serial.println("init : LoRa Module ... done!");
+ } else {
+ Serial.println("Starting LoRa failed!");
+ while (true);
+ }
+ #endif
}
- if (!LoRa.begin(freq)) {
- Serial.println("Starting LoRa failed!");
- show_display("ERROR", "Starting LoRa failed!");
- while (true) {
- delay(1000);
- }
- }
- LoRa.setSpreadingFactor(Config.loramodule.spreadingFactor);
- LoRa.setSignalBandwidth(Config.loramodule.signalBandwidth);
- LoRa.setCodingRate4(Config.loramodule.codingRate4);
- LoRa.enableCrc();
- LoRa.setTxPower(Config.loramodule.power);
- Serial.print("init : LoRa Module ... done!");
- #endif
- #ifdef HAS_SX126X
- SPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN);
- float freq = (float)Config.loramodule.iGateFreq/1000000;
- int state = radio.begin(freq);
- if (state == RADIOLIB_ERR_NONE) {
- Serial.print("Initializing SX126X LoRa Module");
- } else {
- Serial.println("Starting LoRa failed!");
- while (true);
- }
- radio.setDio1Action(setFlag);
- radio.setSpreadingFactor(Config.loramodule.spreadingFactor);
- radio.setBandwidth(Config.loramodule.signalBandwidth);
- radio.setCodingRate(Config.loramodule.codingRate4);
- #if defined(ESP32_DIY_1W_LoRa)
- radio.setRfSwitchPins(RADIO_RXEN, RADIO_TXEN);
- #endif
- #if defined(HELTEC_V3) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2_SX1262)
- state = radio.setOutputPower(Config.loramodule.power + 2); // values available: 10, 17, 22 --> if 20 in tracker_conf.json it will be updated to 22.
- #endif
- #ifdef ESP32_DIY_1W_LoRa_GPS
- state = radio.setOutputPower(Config.loramodule.power); // max value 20 (when 20dB in setup 30dB in output as 400M30S has Low Noise Amp)
- #endif
- if (state == RADIOLIB_ERR_NONE) {
- Serial.println("init : LoRa Module ... done!");
- } else {
- Serial.println("Starting LoRa failed!");
- while (true);
- }
- #endif
- }
- void sendNewPacket(const String &typeOfMessage, const String &newPacket) {
- #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
- digitalWrite(internalLedPin,HIGH);
- #endif
- #ifdef HAS_SX127X
- LoRa.beginPacket();
- LoRa.write('<');
- if (typeOfMessage == "APRS") {
- LoRa.write(0xFF);
- } else if (typeOfMessage == "LoRa") {
- LoRa.write(0xF8);
+ void sendNewPacket(const String &typeOfMessage, const String &newPacket) {
+ #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
+ digitalWrite(internalLedPin,HIGH);
+ #endif
+ #ifdef HAS_SX127X
+ LoRa.beginPacket();
+ LoRa.write('<');
+ if (typeOfMessage == "APRS") {
+ LoRa.write(0xFF);
+ } else if (typeOfMessage == "LoRa") {
+ LoRa.write(0xF8);
+ }
+ LoRa.write(0x01);
+ LoRa.write((const uint8_t *)newPacket.c_str(), newPacket.length());
+ LoRa.endPacket();
+ #endif
+ #ifdef HAS_SX126X
+ int state = radio.transmit("\x3c\xff\x01" + newPacket);
+ if (state == RADIOLIB_ERR_NONE) {
+ //Serial.println(F("success!"));
+ } else if (state == RADIOLIB_ERR_PACKET_TOO_LONG) {
+ Serial.println(F("too long!"));
+ } else if (state == RADIOLIB_ERR_TX_TIMEOUT) {
+ Serial.println(F("timeout!"));
+ } else {
+ Serial.print(F("failed, code "));
+ Serial.println(state);
+ }
+ #endif
+ #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
+ digitalWrite(internalLedPin,LOW);
+ #endif
+ SYSLOG_Utils::log("Tx", newPacket,0,0,0);
+ Serial.print("---> LoRa Packet Tx : ");
+ Serial.println(newPacket);
}
- LoRa.write(0x01);
- LoRa.write((const uint8_t *)newPacket.c_str(), newPacket.length());
- LoRa.endPacket();
- #endif
- #ifdef HAS_SX126X
- int state = radio.transmit("\x3c\xff\x01" + newPacket);
- if (state == RADIOLIB_ERR_NONE) {
- //Serial.println(F("success!"));
- } else if (state == RADIOLIB_ERR_PACKET_TOO_LONG) {
- Serial.println(F("too long!"));
- } else if (state == RADIOLIB_ERR_TX_TIMEOUT) {
- Serial.println(F("timeout!"));
- } else {
- Serial.print(F("failed, code "));
- Serial.println(state);
- }
- #endif
- #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
- digitalWrite(internalLedPin,LOW);
- #endif
- SYSLOG_Utils::log("Tx", newPacket,0,0,0);
- Serial.print("---> LoRa Packet Tx : ");
- Serial.println(newPacket);
- }
- String generatePacket(String aprsisPacket) {
- String firstPart, messagePart;
- aprsisPacket.trim();
- firstPart = aprsisPacket.substring(0, aprsisPacket.indexOf(","));
- messagePart = aprsisPacket.substring(aprsisPacket.indexOf("::")+2);
- return firstPart + ",TCPIP,WIDE1-1," + Config.callsign + "::" + messagePart;
- }
+ String generatePacket(String aprsisPacket) {
+ String firstPart, messagePart;
+ aprsisPacket.trim();
+ firstPart = aprsisPacket.substring(0, aprsisPacket.indexOf(","));
+ messagePart = aprsisPacket.substring(aprsisPacket.indexOf("::")+2);
+ return firstPart + ",TCPIP,WIDE1-1," + Config.callsign + "::" + messagePart;
+ }
- String packetSanitization(String packet) {
- Serial.println(packet);
- if (packet.indexOf("\0")>0) {
- packet.replace("\0","000");
+ String packetSanitization(String packet) {
+ Serial.println(packet);
+ if (packet.indexOf("\0")>0) {
+ packet.replace("\0","");
+ }
+ if (packet.indexOf("\r")>0) {
+ packet.replace("\r","");
+ }
+ if (packet.indexOf("\n")>0) {
+ packet.replace("\n","");
+ }
+ return packet;
}
- if (packet.indexOf("\r")>0) {
- packet.replace("\r","RRR");
- }
- if (packet.indexOf("\n")>0) {
- packet.replace("\n","NNN");
- }
- return packet;
- }
- String receivePacket() {
- String loraPacket = "";
- #ifdef HAS_SX127X
- int packetSize = LoRa.parsePacket();
- if (packetSize) {
- while (LoRa.available()) {
- int inChar = LoRa.read();
- loraPacket += (char)inChar;
- }
- rssi = LoRa.packetRssi();
- snr = LoRa.packetSnr();
- freqError = LoRa.packetFrequencyError();
+ String receivePacket() {
+ String loraPacket = "";
+ #ifdef HAS_SX127X
+ int packetSize = LoRa.parsePacket();
+ if (packetSize) {
+ while (LoRa.available()) {
+ int inChar = LoRa.read();
+ loraPacket += (char)inChar;
+ }
+ rssi = LoRa.packetRssi();
+ snr = LoRa.packetSnr();
+ freqError = LoRa.packetFrequencyError();
+ }
+ #endif
+ #ifdef HAS_SX126X
+ if (transmissionFlag) {
+ transmissionFlag = false;
+ radio.startReceive();
+ int state = radio.readData(loraPacket);
+ if (state == RADIOLIB_ERR_NONE) {
+ Serial.println("LoRa Rx ---> " + loraPacket);
+ rssi = radio.getRSSI();
+ snr = radio.getSNR();
+ freqError = radio.getFrequencyError();
+ } else if (state == RADIOLIB_ERR_RX_TIMEOUT) {
+ // timeout occurred while waiting for a packet
+ } else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
+ Serial.println(F("CRC error!"));
+ } else {
+ Serial.print(F("failed, code "));
+ Serial.println(state);
+ }
+ }
+ #endif
+ // // // // // //
+ if ((loraPacket.indexOf("\0")!=-1) || (loraPacket.indexOf("\r")!=-1) || (loraPacket.indexOf("\n")!=-1)) {
+ loraPacket = packetSanitization(loraPacket);
+ }
+ // // // // // //
+ #ifndef TextSerialOutputForApp
+ if (loraPacket!="") {
+ Serial.println("(RSSI:" +String(rssi) + " / SNR:" + String(snr) + " / FreqErr:" + String(freqError) + ")");
+ }
+ #endif
+ if (Config.syslog.active && (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status()==WL_CONNECTED)) && loraPacket!="") {
+ SYSLOG_Utils::log("Rx", loraPacket, rssi, snr, freqError);
+ }
+ return loraPacket;
}
- #endif
- #ifdef HAS_SX126X
- if (transmissionFlag) {
- transmissionFlag = false;
- radio.startReceive();
- int state = radio.readData(loraPacket);
- if (state == RADIOLIB_ERR_NONE) {
- Serial.println("LoRa Rx ---> " + loraPacket);
- rssi = radio.getRSSI();
- snr = radio.getSNR();
- freqError = radio.getFrequencyError();
- } else if (state == RADIOLIB_ERR_RX_TIMEOUT) {
- // timeout occurred while waiting for a packet
- } else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
- Serial.println(F("CRC error!"));
- } else {
- Serial.print(F("failed, code "));
- Serial.println(state);
- }
- }
- #endif
- // // // // // //
- if ((loraPacket.indexOf("\0")!=-1) || (loraPacket.indexOf("\r")!=-1) || (loraPacket.indexOf("\n")!=-1)) {
- loraPacket = packetSanitization(loraPacket);
- }
- // // // // // //
- #ifndef TextSerialOutputForApp
- if (loraPacket!="") {
- Serial.println("(RSSI:" +String(rssi) + " / SNR:" + String(snr) + " / FreqErr:" + String(freqError) + ")");
- }
- #endif
- if (Config.syslog.active && (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status()==WL_CONNECTED)) && loraPacket!="") {
- SYSLOG_Utils::log("Rx", loraPacket, rssi, snr, freqError);
- }
- return loraPacket;
- }
- void changeFreqTx() {
- delay(500);
- #ifdef HAS_SX127X
- LoRa.setFrequency(Config.loramodule.digirepeaterTxFreq);
- #endif
- #ifdef HAS_SX126X
- float freq = (float)Config.loramodule.digirepeaterTxFreq/1000000;
- radio.setFrequency(freq);
- #endif
- }
+ void changeFreqTx() {
+ delay(500);
+ #ifdef HAS_SX127X
+ LoRa.setFrequency(Config.loramodule.digirepeaterTxFreq);
+ #endif
+ #ifdef HAS_SX126X
+ float freq = (float)Config.loramodule.digirepeaterTxFreq/1000000;
+ radio.setFrequency(freq);
+ #endif
+ }
- void changeFreqRx() {
- delay(500);
- #ifdef HAS_SX127X
- LoRa.setFrequency(Config.loramodule.digirepeaterRxFreq);
- #endif
- #ifdef HAS_SX126X
- float freq = (float)Config.loramodule.digirepeaterRxFreq/1000000;
- radio.setFrequency(freq);
- #endif
- }
+ void changeFreqRx() {
+ delay(500);
+ #ifdef HAS_SX127X
+ LoRa.setFrequency(Config.loramodule.digirepeaterRxFreq);
+ #endif
+ #ifdef HAS_SX126X
+ float freq = (float)Config.loramodule.digirepeaterRxFreq/1000000;
+ radio.setFrequency(freq);
+ #endif
+ }
}
\ No newline at end of file
diff --git a/src/ota_utils.cpp b/src/ota_utils.cpp
new file mode 100644
index 0000000..ffd176e
--- /dev/null
+++ b/src/ota_utils.cpp
@@ -0,0 +1,56 @@
+#include
+#include
+#include
+#include "configuration.h"
+#include "display.h"
+#include "ota_utils.h"
+
+extern Configuration Config;
+extern uint32_t lastScreenOn;
+
+unsigned long ota_progress_millis = 0;
+
+namespace OTA_Utils {
+
+ void setup(AsyncWebServer *server) {
+ if (Config.ota.username != "" && Config.ota.password != "") {
+ ElegantOTA.begin(server, Config.ota.username.c_str(), Config.ota.password.c_str());
+ } else {
+ ElegantOTA.begin(server);
+ }
+
+ ElegantOTA.setAutoReboot(true);
+ ElegantOTA.onStart(onOTAStart);
+ ElegantOTA.onProgress(onOTAProgress);
+ ElegantOTA.onEnd(onOTAEnd);
+ }
+
+ void onOTAStart() {
+ Serial.println("OTA update started!");
+ display_toggle(true);
+ lastScreenOn = millis();
+ show_display("", "", "", " OTA update started!", "", "", "", 1000);
+ }
+
+ void onOTAProgress(size_t current, size_t final) {
+ if (millis() - ota_progress_millis > 1000) {
+ display_toggle(true);
+ lastScreenOn = millis();
+ ota_progress_millis = millis();
+ Serial.printf("OTA Progress Current: %u bytes, Final: %u bytes\n", current, final);
+ show_display("", "", " OTA Progress : " + String((current*100)/final) + "%", "", "", "", "", 100);
+ }
+ }
+
+ void onOTAEnd(bool success) {
+ display_toggle(true);
+ lastScreenOn = millis();
+ if (success) {
+ Serial.println("OTA update finished successfully!");
+ show_display("", "", " OTA update success!", "", " Rebooting ...", "", "", 4000);
+ } else {
+ Serial.println("There was an error during OTA update!");
+ show_display("", "", " OTA update fail!", "", "", "", "", 4000);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ota_utils.h b/src/ota_utils.h
new file mode 100644
index 0000000..3b1acd3
--- /dev/null
+++ b/src/ota_utils.h
@@ -0,0 +1,16 @@
+#ifndef OTA_UTILS_H_
+#define OTA_UTILS_H_
+
+#include
+#include
+
+namespace OTA_Utils {
+
+ void setup(AsyncWebServer *server);
+ void onOTAStart();
+ void onOTAProgress(size_t current, size_t final);
+ void onOTAEnd(bool success);
+
+}
+
+#endif
\ No newline at end of file
diff --git a/src/power_utils.cpp b/src/power_utils.cpp
index c682702..2a3ddee 100644
--- a/src/power_utils.cpp
+++ b/src/power_utils.cpp
@@ -20,124 +20,121 @@ extern Configuration Config;
namespace POWER_Utils {
- bool BatteryIsConnected = false;
- String batteryVoltage = "";
- String batteryChargeDischargeCurrent = "";
+ bool BatteryIsConnected = false;
+ String batteryVoltage = "";
+ String batteryChargeDischargeCurrent = "";
- void activateMeasurement() {
- #if defined(HAS_AXP192) || defined(HAS_AXP2101)
- PMU.disableTSPinMeasure();
- PMU.enableBattDetection();
- PMU.enableVbusVoltageMeasure();
- PMU.enableBattVoltageMeasure();
- PMU.enableSystemVoltageMeasure();
- #endif
- }
-
- void activateLoRa() {
- #ifdef HAS_AXP192
- PMU.setLDO2Voltage(3300);
- PMU.enableLDO2();
- #endif
- #ifdef HAS_AXP2101
- PMU.setALDO2Voltage(3300);
- PMU.enableALDO2();
- #endif
- }
-
- void deactivateLoRa() {
- #ifdef HAS_AXP192
- PMU.disableLDO2();
- #endif
- #ifdef HAS_AXP2101
- PMU.disableALDO2();
- #endif
- }
-
- bool begin(TwoWire &port) {
- #if defined (TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(ESP32_DIY_LoRa) || defined(HELTEC_V3) || defined(ESP32_DIY_1W_LoRa)
- return true;
- #endif
-
- #ifdef HAS_AXP192
- bool result = PMU.begin(Wire, AXP192_SLAVE_ADDRESS, I2C_SDA, I2C_SCL);
- if (result) {
- PMU.disableDC2();
- PMU.disableLDO2();
- PMU.disableLDO3();
- PMU.setDC1Voltage(3300);
- PMU.enableDC1();
- PMU.setProtectedChannel(XPOWERS_DCDC3);
- PMU.disableIRQ(XPOWERS_AXP192_ALL_IRQ);
+ void activateMeasurement() {
+ #if defined(HAS_AXP192) || defined(HAS_AXP2101)
+ PMU.disableTSPinMeasure();
+ PMU.enableBattDetection();
+ PMU.enableVbusVoltageMeasure();
+ PMU.enableBattVoltageMeasure();
+ PMU.enableSystemVoltageMeasure();
+ #endif
}
- return result;
- #endif
- #ifdef HAS_AXP2101
- bool result = PMU.begin(Wire, AXP2101_SLAVE_ADDRESS, I2C_SDA, I2C_SCL);
- if (result) {
- PMU.disableDC2();
- PMU.disableDC3();
- PMU.disableDC4();
- PMU.disableDC5();
- PMU.disableALDO1();
- PMU.disableALDO4();
- PMU.disableBLDO1();
- PMU.disableBLDO2();
- PMU.disableDLDO1();
- PMU.disableDLDO2();
- PMU.setDC1Voltage(3300);
- PMU.enableDC1();
- PMU.setButtonBatteryChargeVoltage(3300);
- PMU.enableButtonBatteryCharge();
- PMU.disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
+ void activateLoRa() {
+ #ifdef HAS_AXP192
+ PMU.setLDO2Voltage(3300);
+ PMU.enableLDO2();
+ #endif
+ #ifdef HAS_AXP2101
+ PMU.setALDO2Voltage(3300);
+ PMU.enableALDO2();
+ #endif
}
- return result;
- #endif
- }
- void setup() {
- Wire.end();
- #ifdef HAS_AXP192
- Wire.begin(SDA, SCL);
- if (begin(Wire)) {
- Serial.println("AXP192 init done!");
- } else {
- Serial.println("AXP192 init failed!");
+ void deactivateLoRa() {
+ #ifdef HAS_AXP192
+ PMU.disableLDO2();
+ #endif
+ #ifdef HAS_AXP2101
+ PMU.disableALDO2();
+ #endif
}
- activateLoRa();
- activateMeasurement();
- PMU.setChargerTerminationCurr(XPOWERS_AXP192_CHG_ITERM_LESS_10_PERCENT);
- PMU.setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
- PMU.setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_780MA);
- PMU.setSysPowerDownVoltage(2600);
- #endif
- #ifdef HAS_AXP2101
- Wire.begin(SDA, SCL);
- if (begin(Wire)) {
- Serial.println("AXP2101 init done!");
- } else {
- Serial.println("AXP2101 init failed!");
+ bool begin(TwoWire &port) {
+ #if defined(HAS_AXP192)
+ bool result = PMU.begin(Wire, AXP192_SLAVE_ADDRESS, I2C_SDA, I2C_SCL);
+ if (result) {
+ PMU.disableDC2();
+ PMU.disableLDO2();
+ PMU.disableLDO3();
+ PMU.setDC1Voltage(3300);
+ PMU.enableDC1();
+ PMU.setProtectedChannel(XPOWERS_DCDC3);
+ PMU.disableIRQ(XPOWERS_AXP192_ALL_IRQ);
+ }
+ return result;
+ #elif defined(HAS_AXP2101)
+ bool result = PMU.begin(Wire, AXP2101_SLAVE_ADDRESS, I2C_SDA, I2C_SCL);
+ if (result) {
+ PMU.disableDC2();
+ PMU.disableDC3();
+ PMU.disableDC4();
+ PMU.disableDC5();
+ PMU.disableALDO1();
+ PMU.disableALDO4();
+ PMU.disableBLDO1();
+ PMU.disableBLDO2();
+ PMU.disableDLDO1();
+ PMU.disableDLDO2();
+ PMU.setDC1Voltage(3300);
+ PMU.enableDC1();
+ PMU.setButtonBatteryChargeVoltage(3300);
+ PMU.enableButtonBatteryCharge();
+ PMU.disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
+ }
+ return result;
+ #else
+ return true;
+ #endif
}
- activateLoRa();
- activateMeasurement();
- PMU.setPrechargeCurr(XPOWERS_AXP2101_PRECHARGE_200MA);
- PMU.setChargerTerminationCurr(XPOWERS_AXP2101_CHG_ITERM_25MA);
- PMU.setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
- PMU.setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_800MA);
- PMU.setSysPowerDownVoltage(2600);
- #endif
- }
- /*void lowerCpuFrequency() {
- #if defined(TTGO_T_Beam_V1_0) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2) || defined(ESP32_DIY_LoRa_GPS) || defined(TTGO_T_LORA32_V2_1_GPS) || defined(TTGO_T_LORA32_V2_1_TNC) || defined(ESP32_DIY_1W_LoRa_GPS) || defined(TTGO_T_Beam_V1_2_SX1262)
- if (setCpuFrequencyMhz(80)) {
- logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "Main", "CPU frequency set to 80MHz");
- } else {
- logger.log(logging::LoggerLevel::LOGGER_LEVEL_WARN, "Main", "CPU frequency unchanged");
+
+ void setup() {
+ Wire.end();
+ #ifdef HAS_AXP192
+ Wire.begin(SDA, SCL);
+ if (begin(Wire)) {
+ Serial.println("AXP192 init done!");
+ } else {
+ Serial.println("AXP192 init failed!");
+ }
+ activateLoRa();
+ activateMeasurement();
+ PMU.setChargerTerminationCurr(XPOWERS_AXP192_CHG_ITERM_LESS_10_PERCENT);
+ PMU.setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
+ PMU.setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_780MA);
+ PMU.setSysPowerDownVoltage(2600);
+ #endif
+
+ #ifdef HAS_AXP2101
+ Wire.begin(SDA, SCL);
+ if (begin(Wire)) {
+ Serial.println("AXP2101 init done!");
+ } else {
+ Serial.println("AXP2101 init failed!");
+ }
+ activateLoRa();
+ activateMeasurement();
+ PMU.setPrechargeCurr(XPOWERS_AXP2101_PRECHARGE_200MA);
+ PMU.setChargerTerminationCurr(XPOWERS_AXP2101_CHG_ITERM_25MA);
+ PMU.setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
+ PMU.setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_800MA);
+ PMU.setSysPowerDownVoltage(2600);
+ #endif
}
- #endif
- }*/
+
+ /*void lowerCpuFrequency() {
+ #if defined(TTGO_T_Beam_V1_0) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2) || defined(ESP32_DIY_LoRa_GPS) || defined(TTGO_T_LORA32_V2_1_GPS) || defined(TTGO_T_LORA32_V2_1_TNC) || defined(ESP32_DIY_1W_LoRa_GPS) || defined(TTGO_T_Beam_V1_2_SX1262)
+ if (setCpuFrequencyMhz(80)) {
+ logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "Main", "CPU frequency set to 80MHz");
+ } else {
+ logger.log(logging::LoggerLevel::LOGGER_LEVEL_WARN, "Main", "CPU frequency unchanged");
+ }
+ #endif
+ }*/
}
\ No newline at end of file
diff --git a/src/power_utils.h b/src/power_utils.h
index b57623b..c7d71df 100644
--- a/src/power_utils.h
+++ b/src/power_utils.h
@@ -6,15 +6,13 @@
namespace POWER_Utils {
- void activateMeasurement();
+ void activateMeasurement();
+ void activateLoRa();
+ void deactivateLoRa();
+ bool begin(TwoWire &port);
+ void setup();
+ //void lowerCpuFrequency();
- void activateLoRa();
- void deactivateLoRa();
-
- bool begin(TwoWire &port);
- void setup();
-
- //void lowerCpuFrequency();
}
#endif
diff --git a/src/query_utils.cpp b/src/query_utils.cpp
index fe093f4..a20f518 100644
--- a/src/query_utils.cpp
+++ b/src/query_utils.cpp
@@ -10,40 +10,40 @@ extern int stationMode;
namespace QUERY_Utils {
- String process(String query, String station, String queryOrigin) {
- String answer;
- if (query=="?APRS?" || query=="?aprs?" || query=="?Aprs?" || query=="H" || query=="h" || query=="HELP" || query=="Help" || query=="help" || query=="?") {
- answer = "?APRSV ?APRSP ?APRSL ?APRSH ?WHERE callsign";
- } else if (query=="?APRSV" || query=="?aprsv" || query=="?Aprsv") {
- answer = "CA2RXU_LoRa_iGate 1.2 v" + versionDate + " sM" + String(stationMode);
- } else if (query=="?APRSP" || query=="?aprsp" || query=="?Aprsp") {
- answer = "iGate QTH: " + String(currentWiFi->latitude,2) + " " + String(currentWiFi->longitude,2);
- } else if (query=="?APRSL" || query=="?aprsl" || query=="?Aprsl") {
- if (lastHeardStation.size() == 0) {
- answer = "No Station Listened in the last " + String(Config.rememberStationTime) + "min.";
- } else {
- for (int i=0; ilatitude,2) + " " + String(currentWiFi->longitude,2);
+ } else if (query=="?APRSL" || query=="?aprsl" || query=="?Aprsl") {
+ if (lastHeardStation.size() == 0) {
+ answer = "No Station Listened in the last " + String(Config.rememberStationTime) + "min.";
+ } else {
+ for (int i=0; i posicion
+ Serial.println("estaciones escuchadas directo (ultimos 30 min)");
+ answer = "?WHERE on development 73!";
+ }
+ for(int i = station.length(); i < 9; i++) {
+ station += ' ';
+ }
+ if (queryOrigin == "APRSIS") {
+ return Config.callsign + ">APLRG1,TCPIP,qAC::" + station + ":" + answer;// + "\n";
+ } else { //} if (queryOrigin == "LoRa") {
+ return Config.callsign + ">APLRG1,RFONLY,WIDE1-1::" + station + ":" + answer;
}
- answer.trim();
- }
- } else if (query.indexOf("?APRSH") == 0 || query.indexOf("?aprsh") == 0 || query.indexOf("?Aprsh") == 0) {
- // sacar callsign despues de ?APRSH
- Serial.println("escuchaste a X estacion? en las ultimas 24 o 8 horas?");
- answer = "APRSH on development 73!";
- } else if (query.indexOf("?WHERE") == 0) {
- // agregar callsign para completar donde esta X callsign --> posicion
- Serial.println("estaciones escuchadas directo (ultimos 30 min)");
- answer = "?WHERE on development 73!";
}
- for(int i = station.length(); i < 9; i++) {
- station += ' ';
- }
- if (queryOrigin == "APRSIS") {
- return Config.callsign + ">APLRG1,TCPIP,qAC::" + station + ":" + answer;// + "\n";
- } else { //} if (queryOrigin == "LoRa") {
- return Config.callsign + ">APLRG1,RFONLY,WIDE1-1::" + station + ":" + answer;
- }
- }
}
\ No newline at end of file
diff --git a/src/station_utils.cpp b/src/station_utils.cpp
index 53bbf68..c45f754 100644
--- a/src/station_utils.cpp
+++ b/src/station_utils.cpp
@@ -12,93 +12,93 @@ extern String fourthLine;
namespace STATION_Utils {
- void deleteNotHeard() {
- for (int i=0; i Listened Station");
- return true;
- }
+ #ifndef TextSerialOutputForApp ////// This is just for debugging
+ Serial.print("Stations Near (last " + String(Config.rememberStationTime) + " minutes): ");
+ for (int k=0; k Station not Heard for last 30 min (Not Tx)\n");
- return false;
- }
- void checkBuffer() {
- for (int i=0; i Listened Station");
+ return true;
+ }
+ }
+ Serial.println(" ---> Station not Heard for last 30 min (Not Tx)\n");
+ return false;
}
- packetBuffer.clear();
- for (int j=0; j") == -1) && (packet.indexOf(":`") == -1)) {
- String sender = packet.substring(3,packet.indexOf(">"));
- String tempAddressee = packet.substring(packet.indexOf("::") + 2);
- String addressee = tempAddressee.substring(0,tempAddressee.indexOf(":"));
- addressee.trim();
- String message = tempAddressee.substring(tempAddressee.indexOf(":")+1);
- //Serial.println(String(millis()) + "," + sender + "," + addressee + "," + message);
- packetBuffer.push_back(String(millis()) + "," + sender + "," + addressee + "," + message);
- checkBuffer();
+ // BORRAR ESTO !!
+ for (int i=0; i") == -1) && (packet.indexOf(":`") == -1)) {
+ String sender = packet.substring(3,packet.indexOf(">"));
+ String tempAddressee = packet.substring(packet.indexOf("::") + 2);
+ String addressee = tempAddressee.substring(0,tempAddressee.indexOf(":"));
+ addressee.trim();
+ String message = tempAddressee.substring(tempAddressee.indexOf(":")+1);
+ //Serial.println(String(millis()) + "," + sender + "," + addressee + "," + message);
+ packetBuffer.push_back(String(millis()) + "," + sender + "," + addressee + "," + message);
+ checkBuffer();
+ }
}
- }
}
\ No newline at end of file
diff --git a/src/utils.cpp b/src/utils.cpp
index fa8a444..233e544 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -1,7 +1,3 @@
-#include
-#include
-#include
-#include
#include
#include "configuration.h"
#include "station_utils.h"
@@ -16,8 +12,6 @@
#include "display.h"
#include "utils.h"
-AsyncWebServer server(80);
-
extern WiFiClient espClient;
extern Configuration Config;
extern String versionDate;
@@ -40,21 +34,13 @@ extern int rssi;
extern float snr;
extern int freqError;
extern String distance;
-extern String versionDate;
extern uint32_t lastWiFiCheck;
extern bool WiFiConnect;
+extern bool WiFiConnected;
-String name;
-String email;
-
-unsigned long ota_progress_millis = 0;
namespace Utils {
- void notFound(AsyncWebServerRequest *request) {
- request->send(404, "text/plain", "Not found");
- }
-
void processStatus() {
String status = Config.callsign + ">APLRG1,WIDE1-1";
if (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status() == WL_CONNECTED)) {
@@ -77,7 +63,11 @@ namespace Utils {
}
String getLocalIP() {
- return "IP : " + String(WiFi.localIP()[0]) + "." + String(WiFi.localIP()[1]) + "." + String(WiFi.localIP()[2]) + "." + String(WiFi.localIP()[3]);
+ if (!WiFiConnected) {
+ return "IP : 192.168.4.1";
+ } else {
+ return "IP : " + String(WiFi.localIP()[0]) + "." + String(WiFi.localIP()[1]) + "." + String(WiFi.localIP()[2]) + "." + String(WiFi.localIP()[3]);
+ }
}
void setupDisplay() {
@@ -85,8 +75,8 @@ namespace Utils {
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
digitalWrite(internalLedPin,HIGH);
#endif
- Serial.println("\nStarting iGate: " + Config.callsign + " Version: " + versionDate);
- show_display(" LoRa APRS", "", " ( iGATE )", "", "", "Richonguzman / CA2RXU", " " + versionDate, 4000);
+ Serial.println("\nStarting Station: " + Config.callsign + " Version: " + versionDate);
+ show_display(" LoRa APRS", "", " ( iGATE & Digi )", "", "", "Richonguzman / CA2RXU", " " + versionDate, 4000);
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
digitalWrite(internalLedPin,LOW);
#endif
@@ -254,9 +244,11 @@ namespace Utils {
void validateDigiFreqs() {
if (stationMode==4) {
if (abs(Config.loramodule.digirepeaterTxFreq - Config.loramodule.digirepeaterRxFreq) < 125000) {
- Serial.println("Tx Freq less than 125kHz from Rx Freq ---> NOT VALID, check 'data/igate_conf.json'");
- show_display("Tx Freq is less than ", "125kHz from Rx Freq", "change it on : /data/", "igate_conf.json", 0);
- while (1);
+ Serial.println("Tx Freq less than 125kHz from Rx Freq ---> NOT VALID");
+ show_display("Tx Freq is less than ", "125kHz from Rx Freq", "device will autofix", "and then reboot", 1000);
+ Config.loramodule.digirepeaterTxFreq = Config.loramodule.digirepeaterRxFreq; // Inform about that but then change the digirepeaterTxFreq to digirepeaterRxFreq and reset the device
+ Config.writeFile();
+ ESP.restart();
}
}
}
@@ -318,83 +310,4 @@ namespace Utils {
}
}
- void onOTAStart() {
- Serial.println("OTA update started!");
- display_toggle(true);
- lastScreenOn = millis();
- show_display("", "", "", " OTA update started!", "", "", "", 1000);
- }
-
- void onOTAProgress(size_t current, size_t final) {
- if (millis() - ota_progress_millis > 1000) {
- display_toggle(true);
- lastScreenOn = millis();
- ota_progress_millis = millis();
- Serial.printf("OTA Progress Current: %u bytes, Final: %u bytes\n", current, final);
- show_display("", "", " OTA Progress : " + String((current*100)/final) + "%", "", "", "", "", 100);
- }
- }
-
- void onOTAEnd(bool success) {
- display_toggle(true);
- lastScreenOn = millis();
- if (success) {
- Serial.println("OTA update finished successfully!");
- show_display("", "", " OTA update success!", "", " Rebooting ...", "", "", 4000);
- } else {
- Serial.println("There was an error during OTA update!");
- show_display("", "", " OTA update fail!", "", "", "", "", 4000);
- }
- }
-
- void startServer() {
- if (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status()==WL_CONNECTED)) {
- server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
- request->send(200, "text/plain", "Hi " + Config.callsign + ", \n\nthis is your (Richonguzman/CA2RXU) LoRa APRS iGate , version " + versionDate + "\n\nTo update your firmware or filesystem go to: http://" + getLocalIP().substring(getLocalIP().indexOf(":")+3) + "/update\n\n\n73!");
- });
-
- server.on("/test", HTTP_GET, [](AsyncWebServerRequest *request) {
- request->send(SPIFFS, "/test_info_1.html", "text/html");//"application/json");
- });
-
- server.on("/test2", HTTP_GET, [](AsyncWebServerRequest *request) {
- request->send(SPIFFS, "/test1.html", "text/html");
- });
-
- if (Config.ota.username != "" && Config.ota.password != "") {
- ElegantOTA.begin(&server, Config.ota.username.c_str(), Config.ota.password.c_str());
- } else {
- ElegantOTA.begin(&server);
- }
- ElegantOTA.setAutoReboot(true);
- ElegantOTA.onStart(onOTAStart);
- ElegantOTA.onProgress(onOTAProgress);
- ElegantOTA.onEnd(onOTAEnd);
-
- server.on("/process_form.php", HTTP_POST, [](AsyncWebServerRequest *request){
- String message;
-
- if (request->hasParam("email", true) && request->hasParam("name", true)) {
- email = request->getParam("email", true)->value();
- name = request->getParam("name", true)->value();
-
- String responseMessage = "Received EMAIL: " + email + ", NAME: " + name;
-
- // Assuming you're sending an HTTP response, for example, in an HTTP server context
- request->send(200, "text/plain", responseMessage);
- } else {
- // Handle the case where one or both parameters are missing
- request->send(400, "text/plain", "Both EMAIL and NAME parameters are required.");
- }
- });
-
- server.onNotFound(notFound);
-
- server.serveStatic("/", SPIFFS, "/");
-
- server.begin();
- Serial.println("init : OTA Server ... done!");
- }
- }
-
}
\ No newline at end of file
diff --git a/src/utils.h b/src/utils.h
index 701ea1d..4cefb9c 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -17,7 +17,6 @@ namespace Utils {
void onOTAStart();
void onOTAProgress(size_t current, size_t final);
void onOTAEnd(bool success);
- void startServer();
}
diff --git a/src/web_utils.cpp b/src/web_utils.cpp
new file mode 100644
index 0000000..e9f0647
--- /dev/null
+++ b/src/web_utils.cpp
@@ -0,0 +1,192 @@
+#include "ota_utils.h"
+#include "web_utils.h"
+#include "configuration.h"
+
+extern Configuration Config;
+
+extern const char web_index_html[] asm("_binary_data_embed_index_html_gz_start");
+extern const char web_index_html_end[] asm("_binary_data_embed_index_html_gz_end");
+extern const size_t web_index_html_len = web_index_html_end - web_index_html;
+
+extern const char web_style_css[] asm("_binary_data_embed_style_css_gz_start");
+extern const char web_style_css_end[] asm("_binary_data_embed_style_css_gz_end");
+extern const size_t web_style_css_len = web_style_css_end - web_style_css;
+
+extern const char web_script_js[] asm("_binary_data_embed_script_js_gz_start");
+extern const char web_script_js_end[] asm("_binary_data_embed_script_js_gz_end");
+extern const size_t web_script_js_len = web_script_js_end - web_script_js;
+
+extern const char web_bootstrap_css[] asm("_binary_data_embed_bootstrap_css_gz_start");
+extern const char web_bootstrap_css_end[] asm("_binary_data_embed_bootstrap_css_gz_end");
+extern const size_t web_bootstrap_css_len = web_bootstrap_css_end - web_bootstrap_css;
+
+extern const char web_bootstrap_js[] asm("_binary_data_embed_bootstrap_js_gz_start");
+extern const char web_bootstrap_js_end[] asm("_binary_data_embed_bootstrap_js_gz_end");
+extern const size_t web_bootstrap_js_len = web_bootstrap_js_end - web_bootstrap_js;
+
+namespace WEB_Utils {
+
+ AsyncWebServer server(80);
+
+ void loop() {
+
+ }
+
+ void handleNotFound(AsyncWebServerRequest *request) {
+ AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", "Not found");
+ response->addHeader("Cache-Control", "max-age=3600");
+ request->send(response);
+ }
+
+ void handleStatus(AsyncWebServerRequest *request) {
+ request->send(200, "text/plain", "OK");
+ }
+
+ void handleHome(AsyncWebServerRequest *request) {
+ AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", (const uint8_t*)web_index_html, web_index_html_len);
+ response->addHeader("Content-Encoding", "gzip");
+ request->send(response);
+ }
+
+ void handleReadConfiguration(AsyncWebServerRequest *request) {
+ File file = SPIFFS.open("/igate_conf.json");
+
+ String fileContent;
+ while(file.available()){
+ fileContent += String((char)file.read());
+ }
+
+ request->send(200, "application/json", fileContent);
+ }
+
+ void handleWriteConfiguration(AsyncWebServerRequest *request) {
+ Serial.println("Got new config from www");
+
+ int networks = request->getParam("wifi.APs", true)->value().toInt();
+
+ Config.wifiAPs = {};
+
+ for (int i=0; igetParam("wifi.AP." + String(i) + ".ssid", true)->value();
+ wifiap.password = request->getParam("wifi.AP." + String(i) + ".password", true)->value();
+ wifiap.latitude = request->getParam("wifi.AP." + String(i) + ".latitude", true)->value().toDouble();
+ wifiap.longitude = request->getParam("wifi.AP." + String(i) + ".longitude", true)->value().toDouble();
+
+ Config.wifiAPs.push_back(wifiap);
+ }
+
+ Config.callsign = request->getParam("callsign", true)->value();
+ Config.stationMode = request->getParam("stationMode", true)->value().toInt();
+ Config.iGateComment = request->getParam("iGateComment", true)->value();
+
+ Config.wifiAutoAP.password = request->getParam("wifi.autoAP.password", true)->value();
+ Config.wifiAutoAP.powerOff = request->getParam("wifi.autoAP.powerOff", true)->value().toInt();
+
+ Config.digi.comment = request->getParam("digi.comment", true)->value();
+ Config.digi.latitude = request->getParam("digi.latitude", true)->value().toDouble();
+ Config.digi.longitude = request->getParam("digi.longitude", true)->value().toDouble();
+
+ Config.aprs_is.passcode = request->getParam("aprs_is.passcode", true)->value();
+ Config.aprs_is.server = request->getParam("aprs_is.server", true)->value();
+ Config.aprs_is.port = request->getParam("aprs_is.port", true)->value().toInt();
+ Config.aprs_is.reportingDistance = request->getParam("aprs_is.reportingDistance", true)->value().toInt();
+
+ Config.loramodule.iGateFreq = request->getParam("lora.iGateFreq", true)->value().toInt();
+
+ if (request->hasParam("lora.digirepeaterTxFreq", true)) {
+ Config.loramodule.digirepeaterTxFreq = request->getParam("lora.digirepeaterTxFreq", true)->value().toInt();
+ }
+
+ if (request->hasParam("lora.digirepeaterRxFreq", true)) {
+ Config.loramodule.digirepeaterRxFreq = request->getParam("lora.digirepeaterRxFreq", true)->value().toInt();
+ }
+
+ Config.loramodule.spreadingFactor = request->getParam("lora.spreadingFactor", true)->value().toInt();
+ Config.loramodule.signalBandwidth = request->getParam("lora.signalBandwidth", true)->value().toInt();
+ Config.loramodule.codingRate4 = request->getParam("lora.codingRate4", true)->value().toInt();
+ Config.loramodule.power = request->getParam("lora.power", true)->value().toInt();
+
+ Config.display.alwaysOn = request->hasParam("display.alwaysOn", true);
+
+ if (!Config.display.alwaysOn) {
+ Config.display.timeout = request->getParam("display.timeout", true)->value().toInt();
+ }
+
+ Config.display.turn180 = request->hasParam("display.turn180", true);
+
+ Config.syslog.active = request->hasParam("syslog.active", true);
+
+ if (Config.syslog.active) {
+ Config.syslog.server = request->getParam("syslog.server", true)->value();
+ Config.syslog.port = request->getParam("syslog.port", true)->value().toInt();
+ }
+
+ Config.bme.active = request->hasParam("bme.active", true);
+
+ Config.ota.username = request->getParam("ota.username", true)->value();
+ Config.ota.password = request->getParam("ota.password", true)->value();
+
+ Config.beaconInterval = request->getParam("other.beaconInterval", true)->value().toInt();
+ Config.igateSendsLoRaBeacons = request->hasParam("other.igateSendsLoRaBeacons", true);
+ Config.igateRepeatsLoRaPackets = request->hasParam("other.igateRepeatsLoRaPackets", true);
+ Config.rememberStationTime = request->getParam("other.rememberStationTime", true)->value().toInt();
+ Config.sendBatteryVoltage = request->hasParam("other.sendBatteryVoltage", true);
+ Config.externalVoltageMeasurement = request->hasParam("other.externalVoltageMeasurement", true);
+
+ if (Config.externalVoltageMeasurement) {
+ Config.externalVoltagePin = request->getParam("other.externalVoltagePin", true)->value().toInt();
+ }
+
+ Config.writeFile();
+
+ AsyncWebServerResponse *response = request->beginResponse(302, "text/html", "");
+ response->addHeader("Location", "/");
+ request->send(response);
+
+ ESP.restart();
+ }
+
+ void handleStyle(AsyncWebServerRequest *request) {
+ AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", (const uint8_t*)web_style_css, web_style_css_len);
+ response->addHeader("Content-Encoding", "gzip");
+ request->send(response);
+ }
+
+ void handleScript(AsyncWebServerRequest *request) {
+ AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", (const uint8_t*)web_script_js, web_script_js_len);
+ response->addHeader("Content-Encoding", "gzip");
+ request->send(response);
+ }
+
+ void handleBootstrapStyle(AsyncWebServerRequest *request) {
+ AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", (const uint8_t*)web_bootstrap_css, web_bootstrap_css_len);
+ response->addHeader("Content-Encoding", "gzip");
+ response->addHeader("Cache-Control", "max-age=3600");
+ request->send(response);
+ }
+
+ void handleBootstrapScript(AsyncWebServerRequest *request) {
+ AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", (const uint8_t*)web_bootstrap_js, web_bootstrap_js_len);
+ response->addHeader("Content-Encoding", "gzip");
+ response->addHeader("Cache-Control", "max-age=3600");
+ request->send(response);
+ }
+
+ void setup() {
+ server.on("/", HTTP_GET, handleHome);
+ server.on("/status", HTTP_GET, handleStatus);
+ server.on("/configuration.json", HTTP_GET, handleReadConfiguration);
+ server.on("/configuration.json", HTTP_POST, handleWriteConfiguration);
+ server.on("/style.css", HTTP_GET, handleStyle);
+ server.on("/script.js", HTTP_GET, handleScript);
+ server.on("/bootstrap.css", HTTP_GET, handleBootstrapStyle);
+ server.on("/bootstrap.js", HTTP_GET, handleBootstrapScript);
+
+ OTA_Utils::setup(&server); // Include OTA Updater for WebServer
+
+ server.onNotFound(handleNotFound);
+
+ server.begin();
+ }
+}
diff --git a/src/web_utils.h b/src/web_utils.h
new file mode 100644
index 0000000..c9dd7b4
--- /dev/null
+++ b/src/web_utils.h
@@ -0,0 +1,31 @@
+#ifndef WEB_UTILS_H_
+#define WEB_UTILS_H_
+
+#include
+#include
+#include
+#include
+#include
+
+
+namespace WEB_Utils {
+
+ void loop();
+
+ void handleNotFound(AsyncWebServerRequest *request);
+ void handleStatus(AsyncWebServerRequest *request);
+ void handleHome(AsyncWebServerRequest *request);
+
+ //void handleReadConfiguration(AsyncWebServerRequest *request);
+ //void handleWriteConfiguration(AsyncWebServerRequest *request);
+
+ void handleStyle(AsyncWebServerRequest *request);
+ void handleScript(AsyncWebServerRequest *request);
+ void handleBootstrapStyle(AsyncWebServerRequest *request);
+ void handleBootstrapScript(AsyncWebServerRequest *request);
+
+ void setup();
+
+}
+
+#endif
\ No newline at end of file
diff --git a/src/wifi_utils.cpp b/src/wifi_utils.cpp
index 777d952..e3a3284 100644
--- a/src/wifi_utils.cpp
+++ b/src/wifi_utils.cpp
@@ -10,93 +10,141 @@ extern int myWiFiAPIndex;
extern int myWiFiAPSize;
extern int stationMode;
extern uint32_t previousWiFiMillis;
+extern bool WiFiConnected;
+extern long WiFiAutoAPTime;
+extern bool WiFiAutoAPStarted;
namespace WIFI_Utils {
- void checkWiFi() {
- if ((WiFi.status() != WL_CONNECTED) && ((millis() - previousWiFiMillis) >= 30*1000)) {
- Serial.print(millis());
- Serial.println("Reconnecting to WiFi...");
- WiFi.disconnect();
- WiFi.reconnect();
- previousWiFiMillis = millis();
- }
- }
-
- void startWiFi() {
- int wifiCounter = 0;
- WiFi.mode(WIFI_STA);
- WiFi.disconnect();
- delay(500);
- unsigned long start = millis();
- show_display("", "", "Connecting to Wifi:", "", currentWiFi->ssid + " ...", 0);
- Serial.print("\nConnecting to WiFi '"); Serial.print(currentWiFi->ssid); Serial.println("' ...");
- WiFi.begin(currentWiFi->ssid.c_str(), currentWiFi->password.c_str());
- while (WiFi.status() != WL_CONNECTED && wifiCounter<2) {
- delay(500);
- #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
- digitalWrite(internalLedPin,HIGH);
- #endif
- Serial.print('.');
- delay(500);
- #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
- digitalWrite(internalLedPin,LOW);
- #endif
- if ((millis() - start) > 10000){
- delay(1000);
- if(myWiFiAPIndex >= (myWiFiAPSize-1)) {
- myWiFiAPIndex = 0;
- if (stationMode==5) {
- wifiCounter++;
- }
- } else {
- myWiFiAPIndex++;
+ void checkWiFi() {
+ if ((WiFi.status() != WL_CONNECTED) && ((millis() - previousWiFiMillis) >= 30*1000) && !WiFiAutoAPStarted) {
+ Serial.print(millis());
+ Serial.println("Reconnecting to WiFi...");
+ WiFi.disconnect();
+ WiFi.reconnect();
+ previousWiFiMillis = millis();
}
- currentWiFi = &Config.wifiAPs[myWiFiAPIndex];
- start = millis();
- Serial.print("\nConnecting to WiFi '"); Serial.print(currentWiFi->ssid); Serial.println("' ...");
- show_display("", "", "Connecting to Wifi:", "", currentWiFi->ssid + " ...", 0);
- WiFi.disconnect();
- WiFi.begin(currentWiFi->ssid.c_str(), currentWiFi->password.c_str());
- }
}
- #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
- digitalWrite(internalLedPin,LOW);
- #endif
- if (WiFi.status() == WL_CONNECTED) {
- Serial.print("Connected as ");
- Serial.println(WiFi.localIP());
- show_display("", "", " Connected!!", "" , " loading ...", 1000);
- } else if (WiFi.status() != WL_CONNECTED && stationMode==5) {
- Serial.println("\nNot connected to WiFi! (DigiRepeater Mode)");
- show_display("", "", " WiFi Not Connected!", " DigiRepeater MODE" , " loading ...", 2000);
- }
- }
- void setup() {
- if (stationMode==1 || stationMode==2) {
- if (stationMode==1) {
- Serial.println("stationMode ---> iGate (only Rx)");
- } else {
- Serial.println("stationMode ---> iGate (Rx + Tx)");
- }
- startWiFi();
- btStop();
- } else if (stationMode==3 || stationMode==4) {
- if (stationMode==3) {
- Serial.println("stationMode ---> DigiRepeater (Rx freq == Tx freq)");
- } else {
- Serial.println("stationMode ---> DigiRepeater (Rx freq != Tx freq)");
- }
- WiFi.mode(WIFI_OFF);
- btStop();
- } else if (stationMode==5) {
- Serial.println("stationMode ---> iGate when Wifi/APRS available (DigiRepeater when not)");
- } else {
- Serial.println("stationMode ---> NOT VALID, check '/data/igate_conf.json'");
- show_display("------- ERROR -------", "stationMode Not Valid", "change it on : /data/", "igate_conf.json", 0);
- while (1);
+ void startWiFi() {
+ bool startAP = false;
+ if (currentWiFi->ssid == "") {
+ startAP = true;
+ } else {
+ int wifiCounter = 0;
+ WiFi.mode(WIFI_STA);
+ WiFi.disconnect();
+ delay(500);
+ unsigned long start = millis();
+ show_display("", "", "Connecting to Wifi:", "", currentWiFi->ssid + " ...", 0);
+ Serial.print("\nConnecting to WiFi '"); Serial.print(currentWiFi->ssid); Serial.println("' ...");
+ WiFi.begin(currentWiFi->ssid.c_str(), currentWiFi->password.c_str());
+ while (WiFi.status() != WL_CONNECTED && wifiCounter 10000){
+ delay(1000);
+ if(myWiFiAPIndex >= (myWiFiAPSize-1)) {
+ myWiFiAPIndex = 0;
+ if (stationMode==5) {
+ wifiCounter++;
+ }
+ } else {
+ myWiFiAPIndex++;
+ }
+ wifiCounter++;
+ currentWiFi = &Config.wifiAPs[myWiFiAPIndex];
+ start = millis();
+ Serial.print("\nConnecting to WiFi '"); Serial.print(currentWiFi->ssid); Serial.println("' ...");
+ show_display("", "", "Connecting to Wifi:", "", currentWiFi->ssid + " ...", 0);
+ WiFi.disconnect();
+ WiFi.begin(currentWiFi->ssid.c_str(), currentWiFi->password.c_str());
+ }
+ }
+ }
+ #if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
+ digitalWrite(internalLedPin,LOW);
+ #endif
+ if (WiFi.status() == WL_CONNECTED) {
+ Serial.print("Connected as ");
+ Serial.println(WiFi.localIP());
+ show_display("", "", " Connected!!", "" , " loading ...", 1000);
+ } else if (WiFi.status() != WL_CONNECTED) {
+ startAP = true;
+
+ Serial.println("\nNot connected to WiFi! Starting Auto AP");
+ show_display("", "", " WiFi Not Connected!", "" , " loading ...", 1000);
+ }
+ WiFiConnected = !startAP;
+ if (startAP) {
+ Serial.println("\nNot connected to WiFi! Starting Auto AP");
+ show_display("", "", " Starting Auto AP", " Please connect to it " , " loading ...", 1000);
+
+ WiFi.mode(WIFI_MODE_NULL);
+
+ WiFi.mode(WIFI_AP);
+ WiFi.softAP(Config.callsign + " AP", "1234567890");
+
+ WiFiAutoAPTime = millis();
+ WiFiAutoAPStarted = true;
+ }
+ }
+
+ void checkIfAutoAPShouldPowerOff() {
+ if (WiFiAutoAPStarted && Config.wifiAutoAP.powerOff > 0) {
+ if (WiFi.softAPgetStationNum() > 0) {
+ WiFiAutoAPTime = 0;
+ } else {
+ if (WiFiAutoAPTime == 0) {
+ WiFiAutoAPTime = millis();
+ } else if ((millis() - WiFiAutoAPTime) > Config.wifiAutoAP.powerOff * 60 * 1000) {
+ Serial.println("Stopping auto AP");
+
+ WiFiAutoAPStarted = false;
+ WiFi.softAPdisconnect(true);
+
+ Serial.println("Auto AP stopped (timeout)");
+ }
+ }
+ }
+ }
+
+ void setup() {
+ if (stationMode==1 || stationMode==2) {
+ if (stationMode==1) {
+ Serial.println("stationMode ---> iGate (only Rx)");
+ } else {
+ Serial.println("stationMode ---> iGate (Rx + Tx)");
+ }
+ startWiFi();
+ btStop();
+ } else if (stationMode==3 || stationMode==4) {
+ if (stationMode==3) {
+ Serial.println("stationMode ---> DigiRepeater (Rx freq == Tx freq)");
+ } else {
+ Serial.println("stationMode ---> DigiRepeater (Rx freq != Tx freq)");
+ }
+ WiFi.mode(WIFI_OFF);
+ btStop();
+ } else if (stationMode==5) {
+ Serial.println("stationMode ---> iGate when Wifi/APRS available (DigiRepeater when not)");
+ } else {
+ Serial.println("stationMode ---> NOT VALID");
+ show_display("------- ERROR -------", "stationMode Not Valid", "device will autofix", "and then reboot", 1000);
+
+ Config.stationMode = 1; // Inform about that but then change the station mode to 1 and reset the device
+ Config.writeFile();
+
+ ESP.restart();
+ }
+
}
- }
}
\ No newline at end of file
diff --git a/src/wifi_utils.h b/src/wifi_utils.h
index fc4c5e9..d74eedf 100644
--- a/src/wifi_utils.h
+++ b/src/wifi_utils.h
@@ -7,7 +7,9 @@ namespace WIFI_Utils {
void checkWiFi();
void startWiFi();
+ void checkIfAutoAPShouldPowerOff();
void setup();
+
}
diff --git a/tools/compress.py b/tools/compress.py
new file mode 100644
index 0000000..c2407b0
--- /dev/null
+++ b/tools/compress.py
@@ -0,0 +1,20 @@
+import gzip
+
+files = [
+ 'data_embed/index.html',
+ 'data_embed/script.js',
+ 'data_embed/style.css',
+ 'data_embed/bootstrap.js',
+ 'data_embed/bootstrap.css',
+]
+
+
+for src in files:
+ out = src + ".gz"
+
+
+ with open(src, 'rb') as f:
+ content = f.read()
+
+ with open(out, 'wb') as f:
+ f.write(gzip.compress(content, compresslevel=9))
\ No newline at end of file