diff --git a/.gitignore b/.gitignore
index 497a39a..52a4746 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,7 @@
CMakeLists.txt
CMakeListsPrivate.txt
CMakeListsUser.txt
-cmake-build-*
\ No newline at end of file
+cmake-build-*
+/include/version.h
+/data_embed/*.out
+/src/TTGO_T-Beam_LoRa_APRS.ino.cpp
diff --git a/data_embed/index.html b/data_embed/index.html
new file mode 100644
index 0000000..ecbdb9c
--- /dev/null
+++ b/data_embed/index.html
@@ -0,0 +1,132 @@
+
+
+
+
+
+ TTGO-T-Beam-LoRa-APRS
+
+
+
+
+
+
+
+
+
WiFi Settings
+
+
+
+
+
+
+
+
APRS Settings
+
+
+
+
+
+
+
+
+
+
diff --git a/data_embed/js.js b/data_embed/js.js
new file mode 100644
index 0000000..abe76a7
--- /dev/null
+++ b/data_embed/js.js
@@ -0,0 +1,42 @@
+/**/
+function scanWifi() {
+ let scanBtn = document.getElementById('scan_wifi_btn');
+ let wifiListContainer = document.getElementById("wifi_list");
+ wifiListContainer.innerHTML = 'Scanning...';
+ scanBtn.disabled = true;
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ scanBtn.disabled = false;
+ if (this.readyState == 4 && this.status == 200) {
+ wifiListContainer.innerHTML = this.responseText;
+ const networks_found_list = document.querySelector('#networks_found_list');
+
+ networks_found_list.addEventListener('change', event => {
+ document.getElementById('wifi_ssid').value = networks_found_list.value;
+ });
+ }
+ };
+ xhttp.open("GET", "/scan_wifi", true);
+ xhttp.send();
+}
+
+window.onload = function () {
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ const response = JSON.parse(this.responseText);
+ for (const [key, value] of Object.entries(response)) {
+ let element = document.getElementById(key);
+ if (element){
+ if (element.type && element.type == "checkbox"){
+ element.checked = value;
+ } else {
+ element.value = value;
+ }
+ }
+ }
+ }
+ };
+ xhttp.open("GET", "/cfg", true);
+ xhttp.send();
+};
\ No newline at end of file
diff --git a/data_embed/style.css b/data_embed/style.css
new file mode 100644
index 0000000..53b15b4
--- /dev/null
+++ b/data_embed/style.css
@@ -0,0 +1,843 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+ ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+ line-height: 1.15; /* 1 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+ ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+ margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+ display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+ box-sizing: content-box; /* 1 */
+ height: 0; /* 1 */
+ overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+ border-bottom: none; /* 1 */
+ text-decoration: underline; /* 2 */
+ text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+ border-style: none;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+ overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+ text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+ padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ * `fieldset` elements in all browsers.
+ */
+
+legend {
+ box-sizing: border-box; /* 1 */
+ color: inherit; /* 2 */
+ display: table; /* 1 */
+ max-width: 100%; /* 1 */
+ padding: 0; /* 3 */
+ white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+ vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/* Interactive
+ ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+ display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+ display: list-item;
+}
+
+/* Misc
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+ display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+ display: none;
+}
+
+/*
+* Barebones V3
+* Copyright 2019 Steve Cochran
+*
+* Based of Skeleton by Dave Gamache
+*
+* Free to use under the MIT license.
+*/
+
+/* Table of contents
+––––––––––––––––––––––––––––––––––––––––––––––––––
+- Media Breakpoints
+- Variables
+- Grid
+- Base Styles
+- Typography
+- Links
+- Buttons
+- Forms
+- Lists
+- Code
+- Tables
+- Spacing
+- Utilities
+- Clearing
+- Media Queries
+*/
+
+/* ENV Variables
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+/* Media breakpoint variables for use in media queries
+* Note: this section is currently commented out pending release of
+* final CSS env() spec
+* Breakpoints based on
+* https://medium.freecodecamp.org/the-100-correct-way-to-do-css-breakpoints-88d6a5ba1862
+*
+*/
+
+
+
+/* CSS Variables
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+html {
+
+ /* default theme: light background, dark text, blue accent */
+ --theme-hue: 0; /* white */
+ --accent-hue: 194; /* blue */
+
+ --text-color-richer: hsl(var(--theme-hue), 0%, 5%); /* #0d0d0d */
+ --text-color-normal: hsl(var(--theme-hue), 0%, 13%); /* #222222 text color; button:hover:focus color */
+ --text-color-softer: hsl(var(--theme-hue), 0%, 33%); /* #555555 button color; button:hover border */
+
+ --accent-color: hsl(var(--accent-hue), 86%, 57%); /* #33C3F0 link; button-primary bg+border; textarea,select:focus border */
+ --accent-color-hover: hsl(var(--accent-hue), 76%, 49%); /* #1EAEDB link hover; button-primary:hover:focus bg+border */
+
+ --border-color: hsl(var(--theme-hue), 0%, 73%); /* #bbbbbb button border */
+ --border-color-softer: hsl(var(--theme-hue), 0%, 82%); /* #d1d1d1 textarea,select,code,td,hr border */
+
+ --background-color: white; /* transparent body background; textarea,select background */
+ --background-color-softer: hsl(var(--theme-hue), 0%, 95%);
+ --code-background: hsl(var(--theme-hue), 0%, 95%); /* #f1f1f1 code background*/
+
+ --button-primary-color: white;
+
+
+ /* Note: Skeleton was based off a 10px font sizing for REM */
+ /* 62.5% of typical 16px browser default = 10px */
+ --base-font-size: 62.5%;
+
+ /* Grid Defaults - default to match orig skeleton settings */
+ --grid-max-width: 960px;
+}
+
+/* Dark Theme
+ Note: prefers-color-scheme selector support is still limited, but
+ included for future and an example of defining a different base 'theme'
+*/
+@media (prefers-color-scheme: dark) {
+ :html {
+ /* dark theme: light background, dark text, blue accent */
+ --theme-hue: 0; /* black */
+ --accent-hue: 194; /* blue */
+
+ --text-color-richer: hsl(var(--theme-hue), 0%, 95%); /* */
+ --text-color-normal: hsl(var(--theme-hue), 0%, 80%); /* text color; button:hover:focus color */
+ --text-color-softer: hsl(var(--theme-hue), 0%, 67%); /* button color; button:hover border */
+
+ --accent-color: hsl(var(--accent-hue), 76%, 49%); /* link; button-primary bg+border; textarea,select:focus border */
+ --accent-color-hover: hsl(var(--accent-hue), 86%, 57%); /* link hover; button-primary:hover:focus bg+border */
+
+ --border-color: hsl(var(--theme-hue), 0%, 27%); /* button border */
+ --border-color-softer: hsl(var(--theme-hue), 0%, 20%); /* textarea,select,code,td,hr border */
+
+ --background-color: hsl(var(--theme-hue), 0%, 12%); /* body background; textarea,select background */
+ --background-color-softer: hsl(var(--theme-hue), 0%, 18%);
+ --code-background: hsl(var(--theme-hue), 0%, 5%); /* code background*/
+
+ --button-primary-color: white;
+ }
+
+ img.value-img {
+ filter: invert(0.8);
+ }
+ /* TODO - test dialing back image intensity on dark background
+ img {
+ opacity: .80;
+ transition: opacity .5s ease-in-out;
+ }
+ img:hover {
+ opacity: 1;
+ }
+ */
+}
+
+
+/* Grid
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+/* CSS Grid depends much more on CSS than HTML, so there is less boilerplate
+ than with skeleton. Only basic 1-4 column grids are included.
+ Any additional needs should be made using custom CSS directives */
+
+
+.grid-container {
+ position: relative;
+ max-width: var(--grid-max-width);
+ margin: 0 auto;
+ padding: 20px;
+ text-align: center;
+ display: grid;
+ grid-gap: 20px;
+ gap: 20px;
+
+ /* by default use min 200px wide columns auto-fit into width */
+ grid-template-columns: minmax(200px, 1fr);
+}
+
+/* grids to 3 columns above mobile sizes */
+@media (min-width: 600px) {
+ .grid-container {
+ grid-template-columns: repeat(3, 1fr);
+ padding: 10px 0;
+ }
+
+ /* basic grids */
+ .grid-container.fifths {
+ grid-template-columns: repeat(5, 1fr);
+ }
+ .grid-container.quarters {
+ grid-template-columns: repeat(4, 1fr);
+ }
+ .grid-container.thirds {
+ grid-template-columns: repeat(3, 1fr);
+ }
+ .grid-container.halves {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ .grid-container.full {
+ grid-template-columns: 1fr;
+ }
+
+}
+
+
+
+
+
+
+
+/* Base Styles
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+html {
+ font-size: var(--base-font-size);
+ scroll-behavior: smooth;
+}
+body {
+ font-size: 1.6rem; /* changed from 15px in orig skeleton */
+ line-height: 1.6;
+ font-weight: 400;
+ font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ color: var(--text-color-normal);
+ background-color: var(--background-color);;
+}
+
+
+/* Typography
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+h1, h2, h3, h4, h5, h6 {
+ margin-top: 0;
+ margin-bottom: 2rem;
+ font-weight: 300; }
+h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
+h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
+h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
+h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
+h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
+h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
+
+/* Larger than phablet */
+@media (min-width: 600px) {
+ h1 { font-size: 5.0rem; }
+ h2 { font-size: 4.2rem; }
+ h3 { font-size: 3.6rem; }
+ h4 { font-size: 3.0rem; }
+ h5 { font-size: 2.4rem; }
+ h6 { font-size: 1.5rem; }
+}
+
+p {
+ margin-top: 0; }
+
+
+/* Links
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+a {
+ color: var(--accent-color); }
+a:hover {
+ color: var(--accent-color-hover); }
+
+
+/* Buttons
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.button,
+button,
+input[type="submit"],
+input[type="reset"],
+input[type="button"] {
+ display: inline-block;
+ height: 38px;
+ padding: 0 30px;
+ color: var(--text-color-softer);
+ text-align: center;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 38px;
+ letter-spacing: .1rem;
+ text-transform: uppercase;
+ text-decoration: none;
+ white-space: nowrap;
+ background-color: transparent;
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ cursor: pointer;
+ box-sizing: border-box; }
+.button:hover,
+button:hover,
+input[type="submit"]:hover,
+input[type="reset"]:hover,
+input[type="button"]:hover,
+.button:focus,
+button:focus,
+input[type="submit"]:focus,
+input[type="reset"]:focus,
+input[type="button"]:focus {
+ color: var(--text-color-normal);
+ border-color: var(--text-color-softer);
+ outline: 0; }
+.button.button-primary,
+button.button-primary,
+input[type="submit"].button-primary,
+input[type="reset"].button-primary,
+input[type="button"].button-primary {
+ color: var(--button-primary-color);
+ background-color: var(--accent-color);
+ border-color: var(--accent-color); }
+.button.button-primary:hover,
+button.button-primary:hover,
+input[type="submit"].button-primary:hover,
+input[type="reset"].button-primary:hover,
+input[type="button"].button-primary:hover,
+.button.button-primary:focus,
+button.button-primary:focus,
+input[type="submit"].button-primary:focus,
+input[type="reset"].button-primary:focus,
+input[type="button"].button-primary:focus {
+ color: var(--button-primary-color);
+ background-color: var(--accent-color-hover);
+ border-color: var(--accent-color-hover); }
+
+
+/* Forms
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+input[type="email"],
+input[type="number"],
+input[type="search"],
+input[type="text"],
+input[type="tel"],
+input[type="url"],
+input[type="password"],
+textarea,
+select {
+ height: 38px;
+ padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
+ background-color: var(--background-color);
+ border: 1px solid var(--border-color-softer);
+ border-radius: 4px;
+ box-shadow: none;
+ box-sizing: border-box; }
+/* Removes awkward default styles on some inputs for iOS */
+input[type="email"],
+input[type="number"],
+input[type="search"],
+input[type="text"],
+input[type="tel"],
+input[type="url"],
+input[type="password"],
+input[type="button"],
+input[type="submit"],
+textarea {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none; }
+textarea {
+ min-height: 65px;
+ padding-top: 6px;
+ padding-bottom: 6px; }
+input[type="email"]:focus,
+input[type="number"]:focus,
+input[type="search"]:focus,
+input[type="text"]:focus,
+input[type="tel"]:focus,
+input[type="url"]:focus,
+input[type="password"]:focus,
+textarea:focus,
+select:focus {
+ border: 1px solid var(--accent-color);
+ outline: 0; }
+label,
+legend {
+ display: block;
+ margin-bottom: .5rem;
+ font-weight: 600; }
+fieldset {
+ padding: 0;
+ border-width: 0; }
+input[type="checkbox"],
+input[type="radio"] {
+ display: inline; }
+label > .label-body {
+ display: inline-block;
+ margin-left: .5rem;
+ font-weight: normal; }
+
+
+/* Lists
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+ul {
+ list-style: circle inside; }
+ol {
+ list-style: decimal inside; }
+ol, ul {
+ padding-left: 0;
+ margin-top: 0; }
+ul ul, ul ol, ol ol, ol ul {
+ font-size: 100%;
+ margin: 1rem 0 1rem 3rem;
+ color: var(--text-color-softer);
+}
+li {
+ margin-bottom: 0.5rem; }
+
+
+/* Code
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+code {
+ padding: .2rem .5rem;
+ margin: 0 .2rem;
+ font-size: 90%;
+ white-space: nowrap;
+ background: var(--code-background);
+ border: 1px solid var(--border-color-softer);
+ border-radius: 4px; }
+pre > code {
+ display: block;
+ padding: 1rem 1.5rem;
+ white-space: pre;
+ overflow: auto; }
+
+
+/* Tables
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+th,
+td {
+ padding: 12px 15px;
+ text-align: left;
+ border-bottom: 1px solid var(--border-color-softer); }
+th:first-child,
+td:first-child {
+ padding-left: 0; }
+th:last-child,
+td:last-child {
+ padding-right: 0; }
+
+
+/* Spacing
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+button,
+.button {
+ margin-bottom: 1rem; }
+input,
+textarea,
+select,
+fieldset {
+ margin-bottom: 1.5rem; }
+pre,
+blockquote,
+dl,
+figure,
+table,
+p,
+ul,
+ol,
+form {
+ margin-bottom: 2.5rem; }
+
+
+/* Utilities
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.u-full-width {
+ width: 100%;
+ box-sizing: border-box; }
+.u-max-full-width {
+ max-width: 100%;
+ box-sizing: border-box; }
+.u-pull-right {
+ float: right; }
+.u-pull-left {
+ float: left; }
+.u-align-left {
+ text-align: left; }
+.u-align-right {
+ text-align: right; }
+
+
+/* Misc
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+hr {
+ margin-top: 3rem;
+ margin-bottom: 3.5rem;
+ border-width: 0;
+ border-top: 1px solid var(--border-color-softer); }
+
+
+/* Clearing
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+
+/* Self Clearing Goodness */
+.container:after,
+.row:after,
+.u-cf {
+ content: "";
+ display: table;
+ clear: both; }
+
+
+/* Media Queries
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+/*
+Note: The best way to structure the use of media queries is to create the queries
+near the relevant code. For example, if you wanted to change the styles for buttons
+on small devices, paste the mobile query code up in the buttons section and style it
+there.
+*/
+
+
+/* Larger than mobile (default point when grid becomes active) */
+@media (min-width: 600px) {
+}
+
+/* Larger than phablet */
+@media (min-width: 900px) {
+ .container {
+ max-width: 900px;
+ }
+}
+
+/* Larger than tablet */
+@media (min-width: 1200px) {}
+
+footer {
+text-align: center
+}
+
+.container {
+position: relative;
+margin: 0 auto;
+}
+
+section {
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ margin-top: 5px;
+ padding: 5px;
+}
\ No newline at end of file
diff --git a/include/taskTNC.h b/include/taskTNC.h
index 6099654..70b4546 100644
--- a/include/taskTNC.h
+++ b/include/taskTNC.h
@@ -6,8 +6,11 @@
#include "BluetoothSerial.h"
extern BluetoothSerial SerialBT;
#endif
+#if defined(ENABLE_WIFI)
+ #include "taskWebServer.h"
+#endif
extern QueueHandle_t tncToSendQueue;
extern QueueHandle_t tncReceivedQueue;
-void taskTNC(void *parameter);
+[[noreturn]] void taskTNC(void *parameter);
diff --git a/include/taskWebServer.h b/include/taskWebServer.h
new file mode 100644
index 0000000..43a6092
--- /dev/null
+++ b/include/taskWebServer.h
@@ -0,0 +1,44 @@
+#include
+#include "TTGO_T-Beam_LoRa_APRS_config.h"
+#include
+#include
+#include
+#include
+
+#ifndef TASK_WEBSERVER
+#define TASK_WEBSERVER
+
+
+#define ENABLE_PREFERENCES
+
+extern Preferences preferences;
+#ifdef KISS_PROTOCOL
+ extern WiFiServer tncServer;
+#endif
+// MAX 15 chars for preferenece key!!!
+static const char *const PREF_APRS_CALLSIGN = "aprs_callsign";
+static const char *const PREF_APRS_RELAY_PATH = "aprs_relay_path";
+static const char *const PREF_APRS_RELAY_PATH_INIT = "aprs_relay_init";
+static const char *const PREF_APRS_SYMBOL_TABLE = "aprs_s_table";
+static const char *const PREF_APRS_SYMBOL = "aprs_symbol";
+static const char *const PREF_APRS_COMMENT = "aprs_comment";
+static const char *const PREF_APRS_COMMENT_INIT = "aprs_comm_init";
+static const char *const PREF_APRS_SHOW_ALTITUDE = "aprs_alt";
+static const char *const PREF_APRS_SHOW_ALTITUDE_INIT = "aprs_alt_init";
+static const char *const PREF_APRS_SHOW_BATTERY = "aprs_batt";
+static const char *const PREF_APRS_SHOW_BATTERY_INIT = "aprs_batt_init";
+static const char *const PREF_APRS_LATITUDE_PRESET = "aprs_lat_p";
+static const char *const PREF_APRS_LATITUDE_PRESET_INIT = "aprs_lat_p_init";
+static const char *const PREF_APRS_LONGITUDE_PRESET = "aprs_lon_p";
+static const char *const PREF_APRS_LONGITUDE_PRESET_INIT = "aprs_lon_p_init";
+static const char *const PREF_APRS_FIXED_BEACON_PRESET = "aprs_fixed_beac";
+static const char *const PREF_APRS_FIXED_BEACON_PRESET_INIT = "aprs_fix_b_init";
+static const char *const PREF_APRS_FIXED_BEACON_INTERVAL_PRESET = "aprs_fb_interv";
+static const char *const PREF_APRS_FIXED_BEACON_INTERVAL_PRESET_INIT = "aprs_fb_in_init";
+
+typedef struct {
+ String callsign;
+} tWebServerCfg;
+
+[[noreturn]] void taskWebServer(void *parameter);
+#endif
\ No newline at end of file
diff --git a/platformio.ini b/platformio.ini
index 109fd81..673298d 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -9,39 +9,37 @@
; https://docs.platformio.org/page/projectconf.html
[env]
+platform = espressif32
+framework = arduino
+
monitor_speed = 115200
build_flags = -Wl,--gc-sections,--relax
-lib_deps =
+board_build.partitions = no_ota.csv
+board_build.embed_files =
+ data_embed/index.html.out
+ data_embed/style.css.out
+ data_embed/js.js.out
+extra_scripts =
+ pre:tools/buildscript_versioning.py
+ pre:tools/compress_assets.py
+lib_deps =
RadioHead
TinyGPSPlus
- #DHT sensor library for ESPx
Adafruit SSD1306
Adafruit GFX Library
Adafruit Unified Sensor
AXP202X_Library
- OneWire
- #DallasTemperature
- #adafruit/Adafruit BME280 Library@^2.1.2
-
[env:ttgo-t-beam-v1.0]
-platform = espressif32
board = ttgo-t-beam
-framework = arduino
-build_flags = -DT_BEAM_V1_0
+build_flags = ${env.build_flags} -DT_BEAM_V1_0
[env:ttgo-t-beam-v0.7]
-platform = espressif32
board = ttgo-t-beam
-framework = arduino
-build_flags = -DT_BEAM_V0_7
+build_flags = ${env.build_flags} -DT_BEAM_V0_7
[env:ttgo-lora32-v2]
-platform = espressif32
board = ttgo-lora32-v1
-framework = arduino
[env:ttgo-lora32-v1]
-platform = espressif32
-board = ttgo-lora32-v1
-framework = arduino
\ No newline at end of file
+board = ttgo-lora32-v1
\ No newline at end of file
diff --git a/src/TTGO_T-Beam_LoRa_APRS.ino b/src/TTGO_T-Beam_LoRa_APRS.ino
index f4571aa..23d530f 100644
--- a/src/TTGO_T-Beam_LoRa_APRS.ino
+++ b/src/TTGO_T-Beam_LoRa_APRS.ino
@@ -25,6 +25,10 @@
#ifdef KISS_PROTOCOL
#include "taskTNC.h"
#endif
+#ifdef ENABLE_WIFI
+ #include "taskWebServer.h"
+#endif
+#include "version.h"
// I2C LINES
#define I2C_SDA 21
@@ -63,6 +67,16 @@ boolean gps_state = true;
boolean key_up = true;
boolean t_lock = false;
boolean fixed_beacon_enabled = false;
+#ifdef SHOW_ALT
+boolean showAltitude = true;
+#else
+boolean showAltitude = false;
+#endif
+#ifdef SHOW_BATT
+boolean showBattery = true;
+#else
+boolean showBattery = false;
+#endif
// Variables and Constants
String loraReceivedFrameString = ""; //data on buff is copied to this string
@@ -111,6 +125,12 @@ ulong time_delay = 0;
float average_course[ANGLE_AVGS];
float avg_c_y, avg_c_x;
uint8_t txPower = TXdbmW;
+
+#ifdef ENABLE_WIFI
+tWebServerCfg webServerCfg;
+#endif
+
+
static const adc_atten_t atten = ADC_ATTEN_DB_6;
static const adc_unit_t unit = ADC_UNIT_1;
#ifdef T_BEAM_V1_0
@@ -148,10 +168,9 @@ void prepareAPRSFrame(){
double Tspeed=0, Tcourse=0;
String Speedx, Coursex;
int i;
- #ifdef SHOW_ALT
- String Altx;
- int Talt;
- #endif
+
+ String Altx;
+ int Talt;
Tlat=gps.location.lat();
Tlon=gps.location.lng();
@@ -178,31 +197,31 @@ void prepareAPRSFrame(){
outString += ">APLM0:!";
}
- if(gps_state && gps.location.isValid()){
+ if(gps_state && gps.location.isValid()) {
outString += aprsSymbolTable;
ax25_base91enc(helper_base91, 4, aprs_lat);
- for (i=0; i<4; i++) {
+ for (i = 0; i < 4; i++) {
outString += helper_base91[i];
}
ax25_base91enc(helper_base91, 4, aprs_lon);
- for (i=0; i<4; i++) {
+ for (i = 0; i < 4; i++) {
outString += helper_base91[i];
}
outString += aprsSymbol;
- ax25_base91enc(helper_base91, 1, (uint32_t) Tcourse/4 );
+ ax25_base91enc(helper_base91, 1, (uint32_t) Tcourse / 4);
outString += helper_base91[0];
- ax25_base91enc(helper_base91, 1, (uint32_t) (log1p(Tspeed)/0.07696));
+ ax25_base91enc(helper_base91, 1, (uint32_t) (log1p(Tspeed) / 0.07696));
outString += helper_base91[0];
outString += "H";
- #ifdef SHOW_ALT
- Talt=gps.altitude.meters() * 3.28d;
+ if (showAltitude) {
+ Talt = gps.altitude.meters() * 3.28d;
Altx = Talt;
outString += "/A=";
- for (i = 0; i < (6-Altx.length()); ++i) {
+ for (i = 0; i < (6 - Altx.length()); ++i) {
outString += "0";
}
outString += Talt;
- #endif
+ }
}else{
outString += aprsLatPreset;
outString += aprsSymbolTable;
@@ -210,11 +229,13 @@ void prepareAPRSFrame(){
outString += aprsSymbol;
}
outString += aprsComment;
- #ifdef SHOW_BATT // battery is not frame part move after comment
+
+ if (showBattery) {
outString += " Batt=";
outString += String(BattVolts, 2);
outString += ("V");
- #endif
+ }
+
#ifdef KISS_PROTOCOL
sendToTNC(outString);
#else
@@ -355,8 +376,88 @@ void sendTelemetryFrame() {
// + SETUP --------------------------------------------------------------+//
void setup(){
+ #ifdef DIGI_PATH
+ relay_path = DIGI_PATH;
+ #else
+ relay_path = "";
+ #endif
#ifdef FIXED_BEACON_EN
- fixed_beacon_enabled = true;
+ fixed_beacon_enabled = true;
+ #endif
+
+ #ifdef ENABLE_PREFERENCES
+ int clear_preferences = 0;
+ if(digitalRead(BUTTON)==LOW){
+ clear_preferences = 1;
+ }
+
+ preferences.begin("cfg", false);
+ aprsSymbolTable = preferences.getString(PREF_APRS_SYMBOL_TABLE);
+ if (aprsSymbolTable.isEmpty()){
+ preferences.putString(PREF_APRS_SYMBOL_TABLE, APRS_SYMBOL_TABLE);
+ aprsSymbolTable = preferences.getString(PREF_APRS_SYMBOL_TABLE);
+ }
+
+ aprsSymbol = preferences.getString(PREF_APRS_SYMBOL);
+ if (aprsSymbol.isEmpty()){
+ preferences.putString(PREF_APRS_SYMBOL, APRS_SYMBOL);
+ aprsSymbol = preferences.getString(PREF_APRS_SYMBOL, APRS_SYMBOL);
+ }
+
+ if (!preferences.getBool(PREF_APRS_COMMENT_INIT)){
+ preferences.putBool(PREF_APRS_COMMENT_INIT, true);
+ preferences.putString(PREF_APRS_COMMENT, MY_COMMENT);
+ }
+ aprsComment = preferences.getString(PREF_APRS_COMMENT);
+
+ if (!preferences.getBool(PREF_APRS_RELAY_PATH_INIT)){
+ preferences.putBool(PREF_APRS_RELAY_PATH_INIT, true);
+ preferences.putString(PREF_APRS_RELAY_PATH, DIGI_PATH);
+ }
+ relay_path = preferences.getString(PREF_APRS_RELAY_PATH);
+
+ if (!preferences.getBool(PREF_APRS_SHOW_ALTITUDE_INIT)){
+ preferences.putBool(PREF_APRS_SHOW_ALTITUDE_INIT, true);
+ preferences.putBool(PREF_APRS_SHOW_ALTITUDE, showAltitude);
+ }
+ showAltitude = preferences.getBool(PREF_APRS_SHOW_ALTITUDE);
+
+ if (!preferences.getBool(PREF_APRS_SHOW_BATTERY_INIT)){
+ preferences.putBool(PREF_APRS_SHOW_BATTERY_INIT, true);
+ preferences.putBool(PREF_APRS_SHOW_BATTERY, showBattery);
+ }
+ showBattery = preferences.getBool(PREF_APRS_SHOW_BATTERY);
+
+ if (!preferences.getBool(PREF_APRS_LATITUDE_PRESET_INIT)){
+ preferences.putBool(PREF_APRS_LATITUDE_PRESET_INIT, true);
+ preferences.putString(PREF_APRS_LATITUDE_PRESET, LATIDUDE_PRESET);
+ }
+ aprsLatPreset = preferences.getString(PREF_APRS_LATITUDE_PRESET);
+
+ if (!preferences.getBool(PREF_APRS_LONGITUDE_PRESET_INIT)){
+ preferences.putBool(PREF_APRS_LONGITUDE_PRESET_INIT, true);
+ preferences.putString(PREF_APRS_LONGITUDE_PRESET, LONGITUDE_PRESET);
+ }
+ aprsLonPreset = preferences.getString(PREF_APRS_LONGITUDE_PRESET);
+
+ if (!preferences.getBool(PREF_APRS_FIXED_BEACON_PRESET_INIT)){
+ preferences.putBool(PREF_APRS_FIXED_BEACON_PRESET_INIT, true);
+ preferences.putBool(PREF_APRS_FIXED_BEACON_PRESET, fixed_beacon_enabled);
+ }
+ fixed_beacon_enabled = preferences.getBool(PREF_APRS_FIXED_BEACON_PRESET);
+
+ if (!preferences.getBool(PREF_APRS_FIXED_BEACON_INTERVAL_PRESET_INIT)){
+ preferences.putBool(PREF_APRS_FIXED_BEACON_INTERVAL_PRESET_INIT, true);
+ preferences.putInt(PREF_APRS_FIXED_BEACON_INTERVAL_PRESET, fix_beacon_interval/1000);
+ }
+ fix_beacon_interval = preferences.getInt(PREF_APRS_FIXED_BEACON_INTERVAL_PRESET) * 1000;
+ if (clear_preferences){
+ delay(1000);
+ if(digitalRead(BUTTON)==LOW){
+ clear_preferences = 2;
+ }
+
+ }
#endif
for (int i=0;i RUNNER, "b" => BICYCLE, "<" => MOTORCYCLE, "R" => Recreation Vehicle
#define MY_COMMENT "Lora Tracker" // add your coment here - if empty then no comment is sent
@@ -26,7 +26,12 @@
//#define ENABLE_TNC_SELF_TELEMETRY
//#define LOCAL_KISS_ECHO // echoing KISS frame back
//#define T_BEAM_V1_0 // if enabled t-beam v1.0 disabled t-beam V.0.7
+ // AUTOMATICALLY SET BY platformio.ini env!
//#define KISS_DEBUG
+#define ENABLE_WIFI
+#define NETWORK_TNC_PORT 8001
+//#define ENABLE_WIFI_CLIENT_DEBUG
+
#define MAX_TIME_TO_NEXT_TX 360000L // TRANSMIT INTERVAL set here MAXIMUM time in ms(!) for smart beaconing - minimum time is always 1 min = 60 secs = 60000L !!!
#define FIX_BEACON_INTERVAL 1800000L // Fixed beacon interwal (when GPS is disabled and FIXED_BEACON_EN is enabled) 30min default
diff --git a/src/taskTNC.cpp b/src/taskTNC.cpp
index aef8ad4..8c499c4 100644
--- a/src/taskTNC.cpp
+++ b/src/taskTNC.cpp
@@ -1,20 +1,58 @@
#include "taskTNC.h"
+
#ifdef ENABLE_BLUETOOTH
BluetoothSerial SerialBT;
#endif
-String inTNCData = "";
-QueueHandle_t tncToSendQueue = nullptr;
+
QueueHandle_t tncReceivedQueue = nullptr;
+#ifdef ENABLE_WIFI
+ #define MAX_WIFI_CLIENTS 6
+ WiFiClient * clients[MAX_WIFI_CLIENTS];
+
+ typedef void (*f_connectedClientCallback_t) (WiFiClient *, int, const String *);
+
+ void iterateWifiClients(f_connectedClientCallback_t callback, const String *data){
+ for (int i=0; iconnected()) {
+ callback(client, i, data);
+ } else {
+ #ifdef ENABLE_WIFI_CLIENT_DEBUG
+ Serial.println(String("Disconnected client ") + client->remoteIP().toString() + ":" + client->remotePort());
+ #endif
+ delete client;
+ clients[i] = nullptr;
+ }
+ }
+ }
+ }
+#endif
+#ifdef ENABLE_WIFI
+ #define IN_TNC_BUFFERS (2+MAX_WIFI_CLIENTS)
+#else
+ #define IN_TNC_BUFFERS 2
+#endif
+
+String inTNCDataBuffers[IN_TNC_BUFFERS];
+
+QueueHandle_t tncToSendQueue = nullptr;
+
/**
* Handle incoming TNC KISS data character
* @param character
*/
-void handleKISSData(char character) {
- inTNCData.concat(character);
- if (character == (char) FEND && inTNCData.length() > 3) {
- const String &TNC2DataFrame = decode_kiss(inTNCData);
+void handleKISSData(char character, int bufferIndex) {
+ String *inTNCData = &inTNCDataBuffers[bufferIndex];
+ if (inTNCData->length() == 0 && character != (char) FEND){
+ // kiss frame begins with C0
+ return;
+ }
+ inTNCData->concat(character);
+ if (character == (char) FEND && inTNCData->length() > 3) {
+ const String &TNC2DataFrame = decode_kiss(*inTNCData);
#ifdef LOCAL_KISS_ECHO
Serial.print(inTNCData);
@@ -23,18 +61,30 @@ void handleKISSData(char character) {
SerialBT.print(inTNCData);
}
#endif
+ #ifdef ENABLE_WIFI
+ iterateWifiClients([](WiFiClient *client, const String *data){
+ if (client->connected()){
+ client->print(*data);
+ client->flush();
+ }
+ }, &inTNCData);
+ #endif
#endif
auto *buffer = new String();
buffer->concat(TNC2DataFrame);
if (xQueueSend(tncToSendQueue, &buffer, (1000 / portTICK_PERIOD_MS)) != pdPASS){
delete buffer;
}
- inTNCData = "";
+ inTNCData->clear();
+ }
+ if (inTNCData->length() > 255){
+ // just in case of garbage input reset data
+ inTNCData->clear();
}
}
-void taskTNC(void *parameter) {
+[[noreturn]] void taskTNC(void *parameter) {
tncToSendQueue = xQueueCreate(4,sizeof(String *));
tncReceivedQueue = xQueueCreate(4,sizeof(String *));
String *loraReceivedFrameString = nullptr;
@@ -42,23 +92,75 @@ void taskTNC(void *parameter) {
while (true) {
while (Serial.available() > 0) {
char character = Serial.read();
- handleKISSData(character);
+ handleKISSData(character, 0);
}
#ifdef ENABLE_BLUETOOTH
- if (SerialBT.hasClient()) {
- while (SerialBT.available() > 0) {
- char character = SerialBT.read();
- handleKISSData(character);
+ if (SerialBT.hasClient()) {
+ while (SerialBT.available() > 0) {
+ char character = SerialBT.read();
+ handleKISSData(character, 1);
+ }
}
- }
+ #endif
+ #ifdef ENABLE_WIFI
+ WiFiClient new_client = tncServer.available();
+ if (new_client.connected()){
+ bool new_client_handled = false;
+ for (int i=0; i < MAX_WIFI_CLIENTS; i++) {
+ auto client = clients[i];
+ if (client == nullptr) {
+ client = new WiFiClient(new_client);
+ clients[i] = client;
+ new_client_handled = true;
+ #ifdef ENABLE_WIFI_CLIENT_DEBUG
+ Serial.println(String("New client #") +String(i) + ": " + client->remoteIP().toString() + ":" + client->remotePort());
+ #endif
+ break;
+ }
+ }
+ #ifdef ENABLE_WIFI_CLIENT_DEBUG
+ for (int i = 0; i < MAX_WIFI_CLIENTS; ++i) {
+ auto client = clients[i];
+
+ if (client != nullptr){
+ Serial.println(String("Client #") +String(i) + ": " + client->remoteIP().toString() + ":" + client->remotePort());
+ }
+ }
+ #endif
+
+
+ if (!new_client_handled){
+ #ifdef ENABLE_WIFI_CLIENT_DEBUG
+ Serial.println(String("Refusing client "));
+ #endif
+ new_client.stop();
+ }
+ }
+ iterateWifiClients([](WiFiClient * client, int clientIdx, const String * unused){
+ while (client->available() > 0) {
+ char character = client->read();
+ handleKISSData(character, 2+clientIdx);
+ }
+ }, nullptr);
+
#endif
if (xQueueReceive(tncReceivedQueue, &loraReceivedFrameString, (1 / portTICK_PERIOD_MS)) == pdPASS) {
- Serial.print(encode_kiss(*loraReceivedFrameString));
+ const String &kissEncoded = encode_kiss(*loraReceivedFrameString);
+ Serial.print(kissEncoded);
#ifdef ENABLE_BLUETOOTH
if (SerialBT.hasClient()){
- SerialBT.print(encode_kiss(*loraReceivedFrameString));
+ SerialBT.print(kissEncoded);
}
#endif
+ #ifdef ENABLE_WIFI
+ iterateWifiClients([](WiFiClient *client, int clientIdx, const String *data){
+ if (client->connected()){
+ client->print(*data);
+ client->flush();
+ }
+ }, &kissEncoded);
+ #endif
+
delete loraReceivedFrameString;
}
vTaskDelay(50 / portTICK_PERIOD_MS);
diff --git a/src/taskWebServer.cpp b/src/taskWebServer.cpp
new file mode 100644
index 0000000..c2667c2
--- /dev/null
+++ b/src/taskWebServer.cpp
@@ -0,0 +1,210 @@
+#include "taskWebServer.h"
+/**
+ * @see board_build.embed_txtfiles in platformio.ini
+ */
+extern const char web_index_html[] asm("_binary_data_embed_index_html_out_start");
+extern const char web_index_html_end[] asm("_binary_data_embed_index_html_out_end");
+extern const char web_style_css[] asm("_binary_data_embed_style_css_out_start");
+extern const char web_style_css_end[] asm("_binary_data_embed_style_css_out_end");
+extern const char web_js_js[] asm("_binary_data_embed_js_js_out_start");
+extern const char web_js_js_end[] asm("_binary_data_embed_js_js_out_end");
+
+
+String apSSID = "";
+String apPassword = "xxxxxxxxxx";
+WebServer server(80);
+Preferences preferences;
+#ifdef KISS_PROTOCOL
+ WiFiServer tncServer(NETWORK_TNC_PORT);
+#endif
+
+void sendCacheHeader() { server.sendHeader("Cache-Control", "max-age=3600"); }
+void sendGzipHeader() { server.sendHeader("Content-Encoding", "gzip"); }
+
+String jsonEscape(String s){
+ s.replace("\"", "\\\"");
+ s.replace("\\", "\\\\");
+ return s;
+}
+
+String jsonLineFromPreferenceString(const char *preferenceName, bool last=false){
+ return String("\"") + preferenceName + "\":\"" + jsonEscape(preferences.getString(preferenceName)) + (last ? + R"(")" : + R"(",)");
+}
+String jsonLineFromPreferenceBool(const char *preferenceName, bool last=false){
+ return String("\"") + preferenceName + "\":" + (preferences.getBool(preferenceName) ? "true" : "false") + (last ? + R"()" : + R"(,)");
+}
+String jsonLineFromPreferenceInt(const char *preferenceName, bool last=false){
+ return String("\"") + preferenceName + "\":" + (preferences.getInt(preferenceName)) + (last ? + R"()" : + R"(,)");
+}
+String jsonLineFromString(const char *name, const char *value, bool last=false){
+ return String("\"") + name + "\":" + jsonEscape(value) + (last ? + R"()" : + R"(,)");
+}
+
+void handle_NotFound(){
+ sendCacheHeader();
+ server.send(404, "text/plain", "Not found");
+}
+
+void handle_Index() {
+ sendGzipHeader();
+ server.send_P(200, "text/html", web_index_html, web_index_html_end - web_index_html);
+}
+
+void handle_Style() {
+ sendCacheHeader();
+ sendGzipHeader();
+ server.send_P(200, "text/css", web_style_css, web_style_css_end - web_style_css);
+}
+
+void handle_Js() {
+ sendCacheHeader();
+ sendGzipHeader();
+ server.send_P(200, "text/javascript", web_js_js, web_js_js_end-web_js_js);
+}
+
+void handle_ScanWifi() {
+ String listResponse = R"(";
+ server.send(200,"text/html", listResponse);
+}
+
+void handle_SaveWifiCfg() {
+ if (!server.hasArg("wifi_ssid") || !server.hasArg("wifi_password")){
+ server.send(500, "text/plain", "Invalid request");
+ }
+ if (!server.arg("wifi_ssid").length() || !server.arg("wifi_password").length()){
+ server.send(403, "text/plain", "Empty SSID or Password");
+ }
+
+ preferences.putString("wifi_ssid", server.arg("wifi_ssid"));
+ preferences.putString("wifi_password", server.arg("wifi_password"));
+
+ server.sendHeader("Location", "/");
+ server.send(302,"text/html", "");
+}
+
+void handle_Reboot() {
+ server.sendHeader("Location", "/");
+ server.send(302,"text/html", "");
+ ESP.restart();
+}
+
+void handle_Restore() {
+ server.sendHeader("Location", "/");
+ server.send(302,"text/html", "");
+ preferences.clear();
+ preferences.end();
+ ESP.restart();
+}
+
+void handle_Cfg() {
+ String jsonData = "{";
+ jsonData += R"("wifi_ssid":")" + jsonEscape(preferences.getString("wifi_ssid")) + R"(",)";
+ jsonData += R"("wifi_password":")" + jsonEscape((preferences.getString("wifi_password").isEmpty() ? String("") : "*")) + R"(",)";
+ jsonData += jsonLineFromPreferenceString(PREF_APRS_CALLSIGN);
+ jsonData += jsonLineFromPreferenceString(PREF_APRS_RELAY_PATH);
+ jsonData += jsonLineFromPreferenceString(PREF_APRS_SYMBOL_TABLE);
+ jsonData += jsonLineFromPreferenceString(PREF_APRS_SYMBOL);
+ jsonData += jsonLineFromPreferenceString(PREF_APRS_COMMENT);
+ jsonData += jsonLineFromPreferenceString(PREF_APRS_LATITUDE_PRESET);
+ jsonData += jsonLineFromPreferenceString(PREF_APRS_LONGITUDE_PRESET);
+ jsonData += jsonLineFromPreferenceInt(PREF_APRS_FIXED_BEACON_INTERVAL_PRESET);
+ jsonData += jsonLineFromPreferenceBool(PREF_APRS_SHOW_BATTERY);
+ jsonData += jsonLineFromPreferenceBool(PREF_APRS_FIXED_BEACON_PRESET);
+ jsonData += jsonLineFromPreferenceBool(PREF_APRS_SHOW_ALTITUDE);
+ jsonData += jsonLineFromString("FreeHeap", String(ESP.getFreeHeap()).c_str());
+ jsonData += jsonLineFromString("HeapSize", String(ESP.getHeapSize()).c_str());
+ jsonData += jsonLineFromString("FreeSketchSpace", String(ESP.getFreeSketchSpace()).c_str(), true);
+
+ jsonData += "}";
+ server.send(200,"application/json", jsonData);
+}
+
+void handle_SaveAPRSCfg() {
+ if (server.hasArg(PREF_APRS_CALLSIGN) && !server.arg(PREF_APRS_CALLSIGN).isEmpty()){
+ preferences.putString(PREF_APRS_CALLSIGN, server.arg(PREF_APRS_CALLSIGN));
+ }
+ if (server.hasArg(PREF_APRS_SYMBOL_TABLE) && !server.arg(PREF_APRS_SYMBOL_TABLE).isEmpty()){
+ preferences.putString(PREF_APRS_SYMBOL_TABLE, server.arg(PREF_APRS_SYMBOL_TABLE));
+ }
+ if (server.hasArg(PREF_APRS_SYMBOL) && !server.arg(PREF_APRS_SYMBOL).isEmpty()){
+ preferences.putString(PREF_APRS_SYMBOL, server.arg(PREF_APRS_SYMBOL));
+ }
+ if (server.hasArg(PREF_APRS_RELAY_PATH)){
+ preferences.putString(PREF_APRS_RELAY_PATH, server.arg(PREF_APRS_RELAY_PATH));
+ }
+ if (server.hasArg(PREF_APRS_COMMENT)){
+ preferences.putString(PREF_APRS_COMMENT, server.arg(PREF_APRS_COMMENT));
+ }
+ if (server.hasArg(PREF_APRS_LATITUDE_PRESET)){
+ preferences.putString(PREF_APRS_LATITUDE_PRESET, server.arg(PREF_APRS_LATITUDE_PRESET));
+ }
+ if (server.hasArg(PREF_APRS_FIXED_BEACON_INTERVAL_PRESET)){
+ preferences.putInt(PREF_APRS_FIXED_BEACON_INTERVAL_PRESET, server.arg(PREF_APRS_FIXED_BEACON_INTERVAL_PRESET).toInt());
+ }
+ if (server.hasArg(PREF_APRS_LONGITUDE_PRESET)){
+ preferences.putString(PREF_APRS_LONGITUDE_PRESET, server.arg(PREF_APRS_LONGITUDE_PRESET));
+ }
+ preferences.putBool(PREF_APRS_SHOW_BATTERY, server.hasArg(PREF_APRS_SHOW_BATTERY));
+ preferences.putBool(PREF_APRS_SHOW_ALTITUDE, server.hasArg(PREF_APRS_SHOW_ALTITUDE));
+ preferences.putBool(PREF_APRS_FIXED_BEACON_PRESET, server.hasArg(PREF_APRS_FIXED_BEACON_PRESET));
+
+
+ server.sendHeader("Location", "/");
+ server.send(302,"text/html", "");
+
+}
+
+[[noreturn]] void taskWebServer(void *parameter) {
+ auto *webServerCfg = (tWebServerCfg*)parameter;
+ apSSID = webServerCfg->callsign + " AP";
+
+ server.on("/", handle_Index);
+ server.on("/favicon.ico", handle_NotFound);
+ server.on("/style.css", handle_Style);
+ server.on("/js.js", handle_Js);
+ server.on("/scan_wifi", handle_ScanWifi);
+ server.on("/save_wifi_cfg", handle_SaveWifiCfg);
+ server.on("/reboot", handle_Reboot);
+ server.on("/cfg", handle_Cfg);
+ server.on("/save_aprs_cfg", handle_SaveAPRSCfg);
+ server.on("/restore", handle_Restore);
+ server.onNotFound(handle_NotFound);
+
+ String wifi_password = preferences.getString("wifi_password");
+ String wifi_ssid = preferences.getString("wifi_ssid");
+ if (!wifi_password.length() || !wifi_ssid.length()){
+ WiFi.softAP(apSSID.c_str(), apPassword.c_str());
+ } else {
+ WiFi.begin(wifi_ssid.c_str(), wifi_password.c_str());
+ Serial.println("Connecting to " + wifi_ssid);
+ while (WiFi.status() != WL_CONNECTED) {
+ Serial.print("Not connected: ");
+ Serial.println((int)WiFi.status());
+ vTaskDelay(500/portTICK_PERIOD_MS);
+ }
+ }
+
+ server.begin();
+ #ifdef KISS_PROTOCOL
+ tncServer.begin();
+ #endif
+ if (MDNS.begin(webServerCfg->callsign.c_str())) {
+ MDNS.setInstanceName(webServerCfg->callsign + " TTGO LoRa APRS TNC " + TXFREQ + "MHz");
+ MDNS.addService("http", "tcp", 80);
+ #ifdef KISS_PROTOCOL
+ MDNS.addService("kiss-tnc", "tcp", NETWORK_TNC_PORT);
+ #endif
+ }
+
+ while (true){
+ server.handleClient();
+ vTaskDelay(5/portTICK_PERIOD_MS);
+ }
+}
\ No newline at end of file
diff --git a/tools/buildscript_versioning.py b/tools/buildscript_versioning.py
new file mode 100644
index 0000000..72b722f
--- /dev/null
+++ b/tools/buildscript_versioning.py
@@ -0,0 +1,39 @@
+FILENAME_BUILDNO = '.pio/versioning'
+FILENAME_VERSION_H = 'include/version.h'
+version = 'v0.1.'
+
+import datetime
+
+build_no = 0
+try:
+ with open(FILENAME_BUILDNO) as f:
+ build_no = int(f.readline()) + 1
+except:
+ print('Starting build number from 1..')
+ build_no = 1
+with open(FILENAME_BUILDNO, 'w+') as f:
+ f.write(str(build_no))
+ print('Build number: {}'.format(build_no))
+
+version_string = "{} - {}".format(version+str(build_no), datetime.datetime.now())
+hf = """
+#ifndef BUILD_NUMBER
+ #define BUILD_NUMBER "{}"
+#endif
+#ifndef VERSION
+ #define VERSION "{}"
+#endif
+#ifndef VERSION_SHORT
+ #define VERSION_SHORT "{}"
+#endif
+""".format(build_no, version_string, version+str(build_no))
+with open(FILENAME_VERSION_H, 'w+') as f:
+ f.write(hf)
+
+with open("data_embed/index.html", "r") as f:
+ index_html_content = f.read()
+
+index_html_content = index_html_content.replace('', version_string)
+
+with open("data_embed/index.html.out", "w") as f:
+ f.write(index_html_content)
diff --git a/tools/compress_assets.py b/tools/compress_assets.py
new file mode 100644
index 0000000..60a395d
--- /dev/null
+++ b/tools/compress_assets.py
@@ -0,0 +1,13 @@
+import gzip
+assets_list = {
+ 'data_embed/index.html.out': 'data_embed/index.html.out',
+ 'data_embed/js.js': 'data_embed/js.js.out',
+ 'data_embed/style.css': 'data_embed/style.css.out',
+}
+
+
+for src_file_name, out_file_name in assets_list.items():
+ with open(src_file_name, 'rb') as f:
+ content = f.read()
+ with open(out_file_name, 'wb') as f:
+ f.write(gzip.compress(content, compresslevel=9))