Compare commits
94 Commits
board-a
...
797876eeac
| Author | SHA1 | Date | |
|---|---|---|---|
| 797876eeac | |||
| f73c2668e3 | |||
| 55b5296d16 | |||
| a8faa6a441 | |||
| c7188df159 | |||
| ce40257fea | |||
| 649f05d330 | |||
| 7896dddd1d | |||
| aa4012f981 | |||
| af0d704e2e | |||
| a5658e3cf3 | |||
| 9923365184 | |||
| 8ba4a179db | |||
| a1255e8304 | |||
| a75ec53d23 | |||
|
|
11e3e126b0 | ||
| cefe34c7bc | |||
| 388e75864a | |||
| b33db504a3 | |||
| 0765c47e4a | |||
| e7d97c1d6f | |||
|
|
d04ac35126 | ||
| 4bd060ff22 | |||
| 4e6db88f7e | |||
| e3d98ed3dd | |||
| 535bd9388e | |||
| 8bc08569fe | |||
| 1f06c677c6 | |||
| 16a87c0f40 | |||
| 69f7471953 | |||
| e1f232eace | |||
| 274b70dbbe | |||
| 3e7d3cabfe | |||
| 40fd0a667a | |||
| 89e17efcfc | |||
| fb2ab88bc9 | |||
| e07aad2d7d | |||
| 358090db5d | |||
| bfc2b17ed4 | |||
| 6be0512146 | |||
| 6606be456b | |||
| 7f6308bb6f | |||
| 23c72790ef | |||
| a484db06c9 | |||
| 9e00fc1135 | |||
| 44a739a0bd | |||
| 873ccc91c5 | |||
| 39ca2d205a | |||
| 4bbf1339f1 | |||
| ae62a7c8e1 | |||
| 46b0ea7bda | |||
| 26773507d5 | |||
| 0c2b4ac07e | |||
| 557646916d | |||
| 6d83ec1b16 | |||
| 5e0eb8b9db | |||
| f96df50f13 | |||
| c21ca6b15f | |||
| b4d8167e19 | |||
| bb1099c534 | |||
| 1152ef00ac | |||
| 75c3fcd91c | |||
| fcbd4f70c5 | |||
| 37519c809d | |||
| b2c05ad95d | |||
| c392e589a7 | |||
| 41759e92d9 | |||
| 0bae914cc7 | |||
| 8873778e31 | |||
| 3902740a25 | |||
| c2a5116cd2 | |||
| c6f852d634 | |||
| 7b8ba3e12b | |||
| 679f8d297c | |||
| 630d3c4f58 | |||
| feb16beb0f | |||
| be52da0604 | |||
| f44cfa3a6c | |||
| dac41fcc68 | |||
| 043028eb96 | |||
| 69276e4627 | |||
| ab04ff8413 | |||
| a32f43ec35 | |||
| b06eaa8b89 | |||
| f0ca03f8cb | |||
| b642c8baa7 | |||
| f5312dab17 | |||
| 6b87c7b0c4 | |||
| a0f5a1e372 | |||
| 58879931b8 | |||
| 0af0d8d465 | |||
| 2dc5798b0a | |||
| 94e831adbf | |||
| 7b3f960373 |
55
.clang-format
Normal file
55
.clang-format
Normal file
@@ -0,0 +1,55 @@
|
||||
# Generated from CLion C/C++ Code Style settings
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -4
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignOperands: false
|
||||
AlignTrailingComments: false
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: true
|
||||
AfterClass: true
|
||||
AfterControlStatement: true
|
||||
AfterEnum: true
|
||||
AfterFunction: true
|
||||
AfterNamespace: true
|
||||
AfterStruct: true
|
||||
AfterUnion: true
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: true
|
||||
BeforeElse: true
|
||||
BeforeLambdaBody: true
|
||||
BeforeWhile: true
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBraces: Custom
|
||||
BreakConstructorInitializers: AfterColon
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
ColumnLimit: 120
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
IncludeCategories:
|
||||
- Regex: '^<.*'
|
||||
Priority: 1
|
||||
- Regex: '^".*'
|
||||
Priority: 2
|
||||
- Regex: '.*'
|
||||
Priority: 3
|
||||
IncludeIsMainRegex: '([-_](test|unittest))?$'
|
||||
IndentCaseBlocks: true
|
||||
IndentWidth: 4
|
||||
InsertNewlineAtEOF: true
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 2
|
||||
NamespaceIndentation: All
|
||||
PointerAlignment: Left
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesInAngles: false
|
||||
SpacesInConditionalStatement: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
TabWidth: 4
|
||||
...
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
/.idea
|
||||
sdkconfig.old
|
||||
/managed_components
|
||||
/managed_components
|
||||
sdkconfig
|
||||
dependencies.lock
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(odroid-remote-http)
|
||||
project(odroid-power-mate)
|
||||
|
||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
675
LICENSE.md
Normal file
675
LICENSE.md
Normal file
@@ -0,0 +1,675 @@
|
||||
# GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||
<https://fsf.org/>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
## Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom
|
||||
to share and change all versions of a program--to make sure it remains
|
||||
free software for all its users. We, the Free Software Foundation, use
|
||||
the GNU General Public License for most of our software; it applies
|
||||
also to any other work released this way by its authors. You can apply
|
||||
it to your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you
|
||||
have certain responsibilities if you distribute copies of the
|
||||
software, or if you modify it: responsibilities to respect the freedom
|
||||
of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the
|
||||
manufacturer can do so. This is fundamentally incompatible with the
|
||||
aim of protecting users' freedom to change the software. The
|
||||
systematic pattern of such abuse occurs in the area of products for
|
||||
individuals to use, which is precisely where it is most unacceptable.
|
||||
Therefore, we have designed this version of the GPL to prohibit the
|
||||
practice for those products. If such problems arise substantially in
|
||||
other domains, we stand ready to extend this provision to those
|
||||
domains in future versions of the GPL, as needed to protect the
|
||||
freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish
|
||||
to avoid the special danger that patents applied to a free program
|
||||
could make it effectively proprietary. To prevent this, the GPL
|
||||
assures that patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
## TERMS AND CONDITIONS
|
||||
|
||||
### 0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds
|
||||
of works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of
|
||||
an exact copy. The resulting work is called a "modified version" of
|
||||
the earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user
|
||||
through a computer network, with no transfer of a copy, is not
|
||||
conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices" to
|
||||
the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
### 1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work for
|
||||
making modifications to it. "Object code" means any non-source form of
|
||||
a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users can
|
||||
regenerate automatically from other parts of the Corresponding Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that same
|
||||
work.
|
||||
|
||||
### 2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not convey,
|
||||
without conditions so long as your license otherwise remains in force.
|
||||
You may convey covered works to others for the sole purpose of having
|
||||
them make modifications exclusively for you, or provide you with
|
||||
facilities for running those works, provided that you comply with the
|
||||
terms of this License in conveying all material for which you do not
|
||||
control copyright. Those thus making or running the covered works for
|
||||
you must do so exclusively on your behalf, under your direction and
|
||||
control, on terms that prohibit them from making any copies of your
|
||||
copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under the
|
||||
conditions stated below. Sublicensing is not allowed; section 10 makes
|
||||
it unnecessary.
|
||||
|
||||
### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such
|
||||
circumvention is effected by exercising rights under this License with
|
||||
respect to the covered work, and you disclaim any intention to limit
|
||||
operation or modification of the work as a means of enforcing, against
|
||||
the work's users, your or third parties' legal rights to forbid
|
||||
circumvention of technological measures.
|
||||
|
||||
### 4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
### 5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these
|
||||
conditions:
|
||||
|
||||
- a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
- b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under
|
||||
section 7. This requirement modifies the requirement in section 4
|
||||
to "keep intact all notices".
|
||||
- c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
- d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
### 6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms of
|
||||
sections 4 and 5, provided that you also convey the machine-readable
|
||||
Corresponding Source under the terms of this License, in one of these
|
||||
ways:
|
||||
|
||||
- a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
- b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the Corresponding
|
||||
Source from a network server at no charge.
|
||||
- c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
- d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
- e) Convey the object code using peer-to-peer transmission,
|
||||
provided you inform other peers where the object code and
|
||||
Corresponding Source of the work are being offered to the general
|
||||
public at no charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal,
|
||||
family, or household purposes, or (2) anything designed or sold for
|
||||
incorporation into a dwelling. In determining whether a product is a
|
||||
consumer product, doubtful cases shall be resolved in favor of
|
||||
coverage. For a particular product received by a particular user,
|
||||
"normally used" refers to a typical or common use of that class of
|
||||
product, regardless of the status of the particular user or of the way
|
||||
in which the particular user actually uses, or expects or is expected
|
||||
to use, the product. A product is a consumer product regardless of
|
||||
whether the product has substantial commercial, industrial or
|
||||
non-consumer uses, unless such uses represent the only significant
|
||||
mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to
|
||||
install and execute modified versions of a covered work in that User
|
||||
Product from a modified version of its Corresponding Source. The
|
||||
information must suffice to ensure that the continued functioning of
|
||||
the modified object code is in no case prevented or interfered with
|
||||
solely because modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or
|
||||
updates for a work that has been modified or installed by the
|
||||
recipient, or for the User Product in which it has been modified or
|
||||
installed. Access to a network may be denied when the modification
|
||||
itself materially and adversely affects the operation of the network
|
||||
or violates the rules and protocols for communication across the
|
||||
network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
### 7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders
|
||||
of that material) supplement the terms of this License with terms:
|
||||
|
||||
- a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
- b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
- c) Prohibiting misrepresentation of the origin of that material,
|
||||
or requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
- d) Limiting the use for publicity purposes of names of licensors
|
||||
or authors of the material; or
|
||||
- e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
- f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions
|
||||
of it) with contractual assumptions of liability to the recipient,
|
||||
for any liability that these contractual assumptions directly
|
||||
impose on those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions; the
|
||||
above requirements apply either way.
|
||||
|
||||
### 8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your license
|
||||
from a particular copyright holder is reinstated (a) provisionally,
|
||||
unless and until the copyright holder explicitly and finally
|
||||
terminates your license, and (b) permanently, if the copyright holder
|
||||
fails to notify you of the violation by some reasonable means prior to
|
||||
60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
### 9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or run
|
||||
a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
### 10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
### 11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims owned
|
||||
or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within the
|
||||
scope of its coverage, prohibits the exercise of, or is conditioned on
|
||||
the non-exercise of one or more of the rights that are specifically
|
||||
granted under this License. You may not convey a covered work if you
|
||||
are a party to an arrangement with a third party that is in the
|
||||
business of distributing software, under which you make payment to the
|
||||
third party based on the extent of your activity of conveying the
|
||||
work, and under which the third party grants, to any of the parties
|
||||
who would receive the covered work from you, a discriminatory patent
|
||||
license (a) in connection with copies of the covered work conveyed by
|
||||
you (or copies made from those copies), or (b) primarily for and in
|
||||
connection with specific products or compilations that contain the
|
||||
covered work, unless you entered into that arrangement, or that patent
|
||||
license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
### 12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under
|
||||
this License and any other pertinent obligations, then as a
|
||||
consequence you may not convey it at all. For example, if you agree to
|
||||
terms that obligate you to collect a royalty for further conveying
|
||||
from those to whom you convey the Program, the only way you could
|
||||
satisfy both those terms and this License would be to refrain entirely
|
||||
from conveying the Program.
|
||||
|
||||
### 13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
### 14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in
|
||||
detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies that a certain numbered version of the GNU General Public
|
||||
License "or any later version" applies to it, you have the option of
|
||||
following the terms and conditions either of that numbered version or
|
||||
of any later version published by the Free Software Foundation. If the
|
||||
Program does not specify a version number of the GNU General Public
|
||||
License, you may choose any version ever published by the Free
|
||||
Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future versions
|
||||
of the GNU General Public License can be used, that proxy's public
|
||||
statement of acceptance of a version permanently authorizes you to
|
||||
choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
### 15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
|
||||
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
|
||||
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
|
||||
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
|
||||
CORRECTION.
|
||||
|
||||
### 16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
|
||||
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
|
||||
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
|
||||
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
|
||||
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
|
||||
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
### 17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
## How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these
|
||||
terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to
|
||||
attach them to the start of each source file to most effectively state
|
||||
the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper
|
||||
mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands \`show w' and \`show c' should show the
|
||||
appropriate parts of the General Public License. Of course, your
|
||||
program's commands might be different; for a GUI interface, you would
|
||||
use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. For more information on this, and how to apply and follow
|
||||
the GNU GPL, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your
|
||||
program into proprietary programs. If your program is a subroutine
|
||||
library, you may consider it more useful to permit linking proprietary
|
||||
applications with the library. If this is what you want to do, use the
|
||||
GNU Lesser General Public License instead of this License. But first,
|
||||
please read <https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
20
README.md
20
README.md
@@ -20,8 +20,15 @@ This project provides a comprehensive web interface to control power, monitor re
|
||||
|
||||
Before you begin, ensure you have the following installed and configured on your system:
|
||||
|
||||
- **[ESP-IDF (Espressif IoT Development Framework)](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/get-started/index.html)**: This project is developed and tested with ESP-IDF v5.x.
|
||||
- **[ESP-IDF (Espressif IoT Development Framework)](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/get-started/index.html)**: This project is developed and tested with ESP-IDF v5.4 or later.
|
||||
- **[Node.js and npm](https://nodejs.org/)**: Required to build the web application. Node.js LTS version (e.g., 18.x or later) is recommended.
|
||||
- **[Nanopb](https://github.com/nanopb/nanopb)**: Required to build for protobuf.
|
||||
|
||||
### Install dependencies (Ubuntu)
|
||||
|
||||
```bash
|
||||
sudo apt install nodejs npm nanopb
|
||||
```
|
||||
|
||||
## How to Build and Flash
|
||||
|
||||
@@ -70,4 +77,13 @@ Before you begin, ensure you have the following installed and configured on your
|
||||
1. After flashing, the ESP32 will either connect to the pre-configured Wi-Fi network or start an Access Point (APSTA).
|
||||
2. Check the serial monitor logs to find the IP address assigned to the device in STA mode, or the default AP address (usually `192.168.4.1`).
|
||||
3. Open a web browser and navigate to the device's IP address.
|
||||
4. You should now see the ODROID Remote control panel.
|
||||
4. You should now see the ODROID Remote control panel.
|
||||
|
||||
## Docs
|
||||
|
||||
- Hardkernel WiKi: [https://wiki.odroid.com/accessory/powermate](https://wiki.odroid.com/accessory/powermate)
|
||||
|
||||
## Repo
|
||||
|
||||
- Hardkernel Github: [https://github.com/hardkernel/odroid-powermate](https://github.com/hardkernel/odroid-powermate)
|
||||
- Original Repo: [https://github.com/shinys000114/odroid-powermate](https://github.com/shinys000114/odroid-powermate)
|
||||
@@ -1,59 +0,0 @@
|
||||
dependencies:
|
||||
espressif/cmake_utilities:
|
||||
component_hash: 05165f30922b422b4b90c08845e6d449329b97370fbd06309803d8cb539d79e3
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=4.1'
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
version: 1.1.1
|
||||
espressif/led_indicator:
|
||||
component_hash: 5b2531835a989825c0dc94465e3481086473e086dca109b99bea5605d8e70396
|
||||
dependencies:
|
||||
- name: espressif/cmake_utilities
|
||||
registry_url: https://components.espressif.com
|
||||
require: private
|
||||
version: '*'
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=4.0'
|
||||
- name: espressif/led_strip
|
||||
registry_url: https://components.espressif.com
|
||||
require: public
|
||||
version: 2.5.5
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.1.1
|
||||
espressif/led_strip:
|
||||
component_hash: 28c6509a727ef74925b372ed404772aeedf11cce10b78c3f69b3c66799095e2d
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=4.4'
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
version: 2.5.5
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 5.4.0
|
||||
joltwallet/littlefs:
|
||||
component_hash: 8e12955f47e27e6070b76715a96d6c75fc2b44f069e8c33679332d9bdd3120c4
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.20.1
|
||||
direct_dependencies:
|
||||
- espressif/led_indicator
|
||||
- joltwallet/littlefs
|
||||
manifest_hash: 445ef18c991ae952f2f16ffe06a905b6d1414a42286212d7b2459fa32945a09c
|
||||
target: esp32c3
|
||||
version: 2.0.0
|
||||
270
docs/API.md
270
docs/API.md
@@ -1,270 +0,0 @@
|
||||
# ODROID Remote API Documentation
|
||||
|
||||
This document outlines the HTTP REST and WebSocket APIs for communication between the web interface and the ESP32 device.
|
||||
|
||||
---
|
||||
|
||||
## WebSocket API
|
||||
|
||||
The WebSocket API provides a full-duplex communication channel for real-time data, such as sensor metrics and the interactive serial console.
|
||||
|
||||
**Endpoint**: `/ws`
|
||||
|
||||
> **Note**: The server only accepts one WebSocket client at a time. Subsequent connection attempts will be rejected with a `403 Forbidden` error until the active client disconnects.
|
||||
|
||||
### Server-to-Client Messages
|
||||
|
||||
The server pushes messages to the client, which can be either JSON objects or raw binary data. JSON messages always contain a `type` field to identify the payload.
|
||||
|
||||
#### JSON Messages
|
||||
|
||||
| Type | Description | Payload Example |
|
||||
|---------------|---------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| `sensor_data` | Pushed periodically (e.g., every second) with the latest power metrics. | `{"type":"sensor_data", "voltage":12.01, "current":1.52, "power":18.25, "uptime_sec":3600, "timestamp": 1672531200}` |
|
||||
| `wifi_status` | Pushed periodically or on change to update the current Wi-Fi connection status. | `{"type":"wifi_status", "connected":true, "ssid":"MyHome_WiFi", "rssi":-65}` |
|
||||
|
||||
**Field Descriptions:**
|
||||
- `sensor_data`:
|
||||
- `voltage` (float): The measured voltage in Volts (V).
|
||||
- `current` (float): The measured current in Amperes (A).
|
||||
- `power` (float): The calculated power in Watts (W).
|
||||
- `uptime_sec` (integer): The system uptime in seconds.
|
||||
- `timestamp` (integer): A Unix timestamp (seconds) of when the measurement was taken.
|
||||
- `wifi_status`:
|
||||
- `connected` (boolean): `true` if connected to a Wi-Fi network, `false` otherwise.
|
||||
- `ssid` (string): The SSID of the connected network. Null if not connected.
|
||||
- `rssi` (integer): The Received Signal Strength Indicator in dBm. Null if not connected.
|
||||
|
||||
#### Raw Binary Data
|
||||
|
||||
- **Description**: Raw binary data from the ODROID's serial (UART) port is forwarded directly to the client. This is used to display the terminal output.
|
||||
- **Payload**: `(binary data)`
|
||||
|
||||
### Client-to-Server Messages
|
||||
|
||||
The client primarily sends raw binary data, which is interpreted as terminal input.
|
||||
|
||||
- **Description**: Raw binary data representing user input from the web terminal. This data is forwarded directly to the ODROID's serial (UART) port.
|
||||
- **Payload**: `(binary data)`
|
||||
|
||||
---
|
||||
|
||||
## HTTP REST API
|
||||
|
||||
The REST API is used for configuration and to trigger specific actions. All request and response bodies are in `application/json` format.
|
||||
|
||||
### Endpoint: `/api/control`
|
||||
|
||||
Manages power relays and system actions.
|
||||
|
||||
#### `GET /api/control`
|
||||
|
||||
Retrieves the current status of the power relays.
|
||||
|
||||
- **Success Response (200 OK)**
|
||||
```json
|
||||
{
|
||||
"load_12v_on": true,
|
||||
"load_5v_on": false
|
||||
}
|
||||
```
|
||||
- `load_12v_on` (boolean): The state of the main 12V power relay.
|
||||
- `load_5v_on` (boolean): The state of the 5V USB power relay.
|
||||
|
||||
#### `POST /api/control`
|
||||
|
||||
Sets the state of power relays or triggers a power action. You can send one or more commands in a single request.
|
||||
|
||||
- **Request Body Examples**:
|
||||
- To turn the main power on:
|
||||
```json
|
||||
{ "load_12v_on": true }
|
||||
```
|
||||
- To trigger a system reset:
|
||||
```json
|
||||
{ "reset_trigger": true }
|
||||
```
|
||||
- To toggle the power button:
|
||||
```json
|
||||
{ "power_trigger": true }
|
||||
```
|
||||
|
||||
- **Request Fields**:
|
||||
- `load_12v_on` (boolean, optional): Sets the state of the 12V relay.
|
||||
- `load_5v_on` (boolean, optional): Sets the state of the 5V relay.
|
||||
- `reset_trigger` (boolean, optional): If `true`, momentarily triggers the reset button (pulls the line low for 3 seconds). The action is triggered only on a `true` value.
|
||||
- `power_trigger` (boolean, optional): If `true`, momentarily triggers the power button (pulls the line low for 3 seconds). The action is triggered only on a `true` value.
|
||||
|
||||
- **Success Response (200 OK)**: `{"status":"ok"}`
|
||||
|
||||
---
|
||||
|
||||
### Endpoint: `/api/setting`
|
||||
|
||||
Manages all Wi-Fi, network, and system-related configurations.
|
||||
|
||||
#### `GET /api/setting`
|
||||
|
||||
Retrieves the complete current network and system configuration.
|
||||
|
||||
- **Success Response (200 OK)**
|
||||
```json
|
||||
{
|
||||
"connected": true,
|
||||
"ssid": "MyHome_WiFi",
|
||||
"rssi": -65,
|
||||
"mode": "apsta",
|
||||
"net_type": "static",
|
||||
"baudrate": "115200",
|
||||
"ip": {
|
||||
"ip": "192.168.1.100",
|
||||
"gateway": "192.168.1.1",
|
||||
"subnet": "255.255.255.0",
|
||||
"dns1": "8.8.8.8",
|
||||
"dns2": "8.8.4.4"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Response Fields**:
|
||||
- `connected` (boolean): Current Wi-Fi connection state.
|
||||
- `ssid` (string): The SSID of the connected network.
|
||||
- `rssi` (integer): The Received Signal Strength Indicator in dBm. Only present if connected.
|
||||
- `mode` (string): The current Wi-Fi mode (`"sta"` or `"apsta"`).
|
||||
- `net_type` (string): The network type (`"dhcp"` or `"static"`).
|
||||
- `baudrate` (string): The current UART baud rate.
|
||||
- `ip` (object): Contains IP configuration details. Present even if using DHCP (may show the last-leased IP).
|
||||
- `ip` (string): The device's IP address.
|
||||
- `gateway` (string): The network gateway address.
|
||||
- `subnet` (string): The network subnet mask.
|
||||
- `dns1` (string): The primary DNS server address.
|
||||
- `dns2` (string): The secondary DNS server address.
|
||||
|
||||
#### `POST /api/setting`
|
||||
|
||||
This is a multi-purpose endpoint. The server determines the action based on the fields provided in the request body.
|
||||
|
||||
- **Action: Connect to a Wi-Fi Network**
|
||||
- **Request Body**:
|
||||
```json
|
||||
{
|
||||
"ssid": "MyHome_WiFi",
|
||||
"password": "my_secret_password"
|
||||
}
|
||||
```
|
||||
- **Success Response (200 OK)**:
|
||||
```json
|
||||
{ "status": "connection_initiated" }
|
||||
```
|
||||
|
||||
- **Action: Configure Network Type (DHCP/Static)**
|
||||
- **Request Body (for DHCP)**:
|
||||
```json
|
||||
{ "net_type": "dhcp" }
|
||||
```
|
||||
- **Request Body (for Static IP)**:
|
||||
*Note: The `ip` object structure is consistent with the `GET /api/setting` response.*
|
||||
```json
|
||||
{
|
||||
"net_type": "static",
|
||||
"ip": {
|
||||
"ip": "192.168.1.100",
|
||||
"gateway": "192.168.1.1",
|
||||
"subnet": "255.255.255.0",
|
||||
"dns1": "8.8.8.8",
|
||||
"dns2": "8.8.4.4"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Success Response (200 OK)**:
|
||||
- `{"status":"dhcp_config_applied"}`
|
||||
- `{"status":"static_config_applied"}`
|
||||
|
||||
- **Action: Configure Wi-Fi Mode (STA/APSTA)**
|
||||
- **Request Body (for STA mode)**:
|
||||
```json
|
||||
{ "mode": "sta" }
|
||||
```
|
||||
- **Request Body (for AP+STA mode)**:
|
||||
```json
|
||||
{
|
||||
"mode": "apsta",
|
||||
"ap_ssid": "ODROID-Remote-AP",
|
||||
"ap_password": "hardkernel"
|
||||
}
|
||||
```
|
||||
*Note: `ap_password` is optional. If omitted, the AP will be open.*
|
||||
- **Success Response (200 OK)**: `{"status":"mode_switch_initiated"}`
|
||||
|
||||
- **Action: Configure UART Baud Rate**
|
||||
- **Request Body**:
|
||||
```json
|
||||
{ "baudrate": "115200" }
|
||||
```
|
||||
- **Success Response (200 OK)**:
|
||||
```json
|
||||
{ "status": "baudrate_updated" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Endpoint: `/api/wifi/scan`
|
||||
|
||||
Scans for available Wi-Fi networks.
|
||||
|
||||
#### `GET /api/wifi/scan`
|
||||
|
||||
- **Success Response (200 OK)**: Returns a JSON array of found networks.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"ssid": "MyHome_WiFi",
|
||||
"rssi": -55,
|
||||
"authmode": "WPA2_PSK"
|
||||
},
|
||||
{
|
||||
"ssid": "GuestNetwork",
|
||||
"rssi": -78,
|
||||
"authmode": "OPEN"
|
||||
}
|
||||
]
|
||||
```
|
||||
- **Response Fields**:
|
||||
- `ssid` (string): The network's Service Set Identifier.
|
||||
- `rssi` (integer): Signal strength in dBm.
|
||||
- `authmode` (string): The authentication mode (e.g., `"OPEN"`, `"WPA_PSK"`, `"WPA2_PSK"`).
|
||||
|
||||
---
|
||||
|
||||
### Endpoint: `/datalog.csv`
|
||||
|
||||
Provides access to the historical sensor data log.
|
||||
|
||||
#### `GET /datalog.csv`
|
||||
|
||||
- **Description**: Downloads a CSV file containing the history of sensor data readings (voltage, current, power). The log is rotated when it reaches its maximum size (1MB).
|
||||
- **Success Response (200 OK)**: The body of the response is the CSV file content.
|
||||
- **Response Headers**:
|
||||
- `Content-Type: text/csv`
|
||||
- `Content-Disposition: attachment; filename="datalog.csv"`
|
||||
- **CSV Format**:
|
||||
```csv
|
||||
timestamp,voltage,current,power
|
||||
1672531200,12.01,1.52,18.25
|
||||
1672531201,12.02,1.53,18.39
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### General Error Responses
|
||||
|
||||
In case of an error, the server will respond with an appropriate HTTP status code.
|
||||
|
||||
- **`400 Bad Request`**: The request is malformed, contains invalid parameters, or is otherwise incorrect. The response body may contain a JSON object with more details.
|
||||
```json
|
||||
{
|
||||
"error": "Invalid request body"
|
||||
}
|
||||
```
|
||||
- **`404 Not Found`**: The requested endpoint does not exist.
|
||||
- **`500 Internal Server Error`**: The server encountered an unexpected condition that prevented it from fulfilling the request.
|
||||
Binary file not shown.
5
example/logger/.gitignore
vendored
Normal file
5
example/logger/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/.venv/
|
||||
/venv/
|
||||
status_pb2.py
|
||||
test.csv
|
||||
plot.png
|
||||
140
example/logger/README.md
Normal file
140
example/logger/README.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Odroid PowerMate Logger and Plotter
|
||||
|
||||
This directory contains two Python scripts to log power data from an Odroid Smart Power device and visualize it.
|
||||
|
||||
1. `logger.py`: Connects to the device's web server, authenticates, and logs real-time power data from its WebSocket to a CSV file.
|
||||
2. `csv_2_plot.py`: Reads the generated CSV file and creates a plot image of the power, voltage, and current data over time.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. Clone this example
|
||||
```bash
|
||||
git clone https://github.com/hardkernel/odroid-powermate.git
|
||||
cd odroid-powermate/example/logger
|
||||
```
|
||||
|
||||
### 2. Python and Virtual Environment
|
||||
|
||||
It is highly recommended to use a Python virtual environment to manage project dependencies and avoid conflicts with other projects.
|
||||
|
||||
Ensure you have Python 3 installed.
|
||||
|
||||
1. **Create a virtual environment:**
|
||||
Open your terminal in this directory and run:
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
```
|
||||
This will create a `venv` directory containing the Python interpreter and libraries.
|
||||
|
||||
2. **Activate the virtual environment:**
|
||||
* **On Windows:**
|
||||
```powershell
|
||||
.\venv\Scripts\activate
|
||||
```
|
||||
* **On macOS and Linux:**
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
```
|
||||
Your terminal prompt should now show `(venv)` at the beginning, indicating that the virtual environment is active.
|
||||
|
||||
### 3. Install Required Libraries
|
||||
|
||||
With the virtual environment activated, install the necessary Python packages:
|
||||
|
||||
```bash
|
||||
pip33 install requests websockets protobuf pandas matplotlib python-dateutil
|
||||
```
|
||||
|
||||
### 4. Protobuf Generated File
|
||||
|
||||
The `logger.py` script uses Google Protocol Buffers (Protobuf) to decode real-time data from the WebSocket. This requires a Python file, `status_pb2.py`, which is generated from a Protobuf definition file (`status.proto`).
|
||||
|
||||
**How to Generate `status_pb2.py`:**
|
||||
|
||||
1. **Install Protobuf Compiler Tools:**
|
||||
You need the `grpcio-tools` package, which includes the `protoc` compiler and Python plugins. You can install it via pip:
|
||||
```bash
|
||||
pip3 install grpcio-tools
|
||||
```
|
||||
|
||||
2. **Locate the `.proto` file:**
|
||||
Ensure you have the `status.proto` file in the current directory. This file defines the structure of the data messages.
|
||||
|
||||
3. **Run the Compiler:**
|
||||
Execute the following command in your terminal. This command tells `protoc` to look for `status.proto` in the directory (`-I../../proto`) and generate the Python output file (`--python_out=.`) in the same place.
|
||||
```bash
|
||||
python3 -m grpc_tools.protoc -I../../proto --python_out=. status.proto
|
||||
```
|
||||
After running this command, the `status_pb2.py` file will be created, and `logger.py` will be able to use it.
|
||||
|
||||
## Usage
|
||||
|
||||
The process is a two-step workflow: first log the data, then plot it.
|
||||
|
||||
### Step 1: Log Power Data with `logger.py`
|
||||
|
||||
Run `logger.py` to connect to your Odroid Smart Power device and save the data to a CSV file.
|
||||
|
||||
**Syntax:**
|
||||
```bash
|
||||
python3 logger.py <host> -u <username> -p <password> -o <output_file.csv>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
* `host`: The IP address or hostname of the Odroid Smart Power device (e.g., `192.168.1.50`).
|
||||
* `-u`, `--username`: The username for logging in.
|
||||
* `-p`, `--password`: The password for logging in.
|
||||
* `-o`, `--output`: The path to save the output CSV file. This is required if you want to generate a plot.
|
||||
|
||||
**Example:**
|
||||
|
||||
This command will log in and save the power data to `power_log.csv`.
|
||||
|
||||
```bash
|
||||
python3 logger.py 192.168.1.50 -u admin -p mypassword -o power_log.csv
|
||||
```
|
||||
|
||||
The script will continue to log data until you stop it with `Ctrl+C`.
|
||||
|
||||
### Step 2: Generate a Plot with `csv_2_plot.py`
|
||||
|
||||
Once you have a CSV log file, you can use `csv_2_plot.py` to create a visual graph.
|
||||
You can also use the csv file recorded from PowerMate Web.
|
||||
|
||||
**Syntax:**
|
||||
```bash
|
||||
python3 csv_2_plot.py <input.csv> <output.png> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
* `input_csv`: The path to the CSV file generated by `logger.py`.
|
||||
* `output_image`: The path to save the output plot image (e.g., `plot.png`).
|
||||
|
||||
**Optional Arguments:**
|
||||
* `-t`, `--type`: Specify which plots to generate. Choices are `power`, `voltage`, `current`. Default is all three.
|
||||
* `-s`, `--source`: Specify which power sources to include. Choices are `vin`, `main`, `usb`. Default is all three.
|
||||
|
||||
**Example 1: Default Plot**
|
||||
|
||||
This command reads `power_log.csv` and generates a plot containing power, voltage, and current for all sources, saving it as `power_graph.png`.
|
||||
|
||||
```bash
|
||||
python3 csv_2_plot.py power_log.csv power_graph.png
|
||||
```
|
||||
|
||||
**Example 2: Custom Plot**
|
||||
|
||||
This command generates a plot showing only the **power** and **current** for the **MAIN** and **USB** sources.
|
||||
|
||||
```bash
|
||||
# main, usb power consumption
|
||||
python csv_2_plot.py power_log.csv custom_plot.png --type power --source main usb
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
Running the plot script will generate an image file similar to this:
|
||||
|
||||

|
||||
|
||||
The 5-unit scale is highlighted with a blue dotted line, and the 10-unit scale is highlighted with a red dotted line.
|
||||
202
example/logger/csv_2_plot.py
Normal file
202
example/logger/csv_2_plot.py
Normal file
@@ -0,0 +1,202 @@
|
||||
import argparse
|
||||
|
||||
import matplotlib.dates as mdates
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
from dateutil.tz import gettz
|
||||
from matplotlib.ticker import MultipleLocator
|
||||
|
||||
|
||||
def plot_power_data(csv_path, output_path, plot_types, sources):
|
||||
"""
|
||||
Reads power data from a CSV file and generates a plot image.
|
||||
|
||||
Args:
|
||||
csv_path (str): The path to the input CSV file.
|
||||
output_path (str): The path to save the output plot image.
|
||||
plot_types (list): A list of strings indicating which plots to generate
|
||||
(e.g., ['power', 'voltage', 'current']).
|
||||
sources (list): A list of strings indicating which power sources to plot
|
||||
(e.g., ['vin', 'main', 'usb']).
|
||||
"""
|
||||
try:
|
||||
# Read the CSV file into a pandas DataFrame
|
||||
df = pd.read_csv(csv_path, parse_dates=['timestamp'])
|
||||
print(f"Successfully loaded {len(df)} records from '{csv_path}'")
|
||||
|
||||
# --- Timezone Conversion ---
|
||||
local_tz = gettz()
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert(local_tz)
|
||||
print(f"Timestamp converted to local timezone: {local_tz}")
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: The file '{csv_path}' was not found.")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"An error occurred while reading the CSV file: {e}")
|
||||
return
|
||||
|
||||
# --- Calculate Average Interval ---
|
||||
avg_interval_ms = 0
|
||||
if len(df) > 1:
|
||||
avg_interval = df['timestamp'].diff().mean()
|
||||
avg_interval_ms = avg_interval.total_seconds() * 1000
|
||||
|
||||
# --- Calculate Average Voltages ---
|
||||
avg_voltages = {}
|
||||
for source in sources:
|
||||
voltage_col = f'{source}_voltage'
|
||||
if voltage_col in df.columns:
|
||||
avg_voltages[source] = df[voltage_col].mean()
|
||||
|
||||
# --- Plotting Configuration ---
|
||||
scale_config = {
|
||||
'power': {'steps': [5, 20, 50, 160]},
|
||||
'voltage': {'steps': [5, 10, 15, 25]},
|
||||
'current': {'steps': [1, 2.5, 5, 10]}
|
||||
}
|
||||
plot_configs = {
|
||||
'power': {'title': 'Power Consumption', 'ylabel': 'Power (W)', 'cols': [f'{s}_power' for s in sources]},
|
||||
'voltage': {'title': 'Voltage', 'ylabel': 'Voltage (V)', 'cols': [f'{s}_voltage' for s in sources]},
|
||||
'current': {'title': 'Current', 'ylabel': 'Current (A)', 'cols': [f'{s}_current' for s in sources]}
|
||||
}
|
||||
|
||||
channel_labels = [s.upper() for s in sources]
|
||||
color_map = {'vin': 'red', 'main': 'green', 'usb': 'blue'}
|
||||
channel_colors = [color_map[s] for s in sources]
|
||||
|
||||
num_plots = len(plot_types)
|
||||
if num_plots == 0:
|
||||
print("No plot types selected. Exiting.")
|
||||
return
|
||||
|
||||
fig, axes = plt.subplots(num_plots, 1, figsize=(15, 9 * num_plots), sharex=True, squeeze=False)
|
||||
axes = axes.flatten()
|
||||
|
||||
# --- Loop through selected plot types and generate plots ---
|
||||
for i, plot_type in enumerate(plot_types):
|
||||
ax = axes[i]
|
||||
config = plot_configs[plot_type]
|
||||
max_data_value = 0
|
||||
for j, col_name in enumerate(config['cols']):
|
||||
if col_name in df.columns:
|
||||
ax.plot(df['timestamp'], df[col_name], label=channel_labels[j], color=channel_colors[j], zorder=2)
|
||||
max_col_value = df[col_name].max()
|
||||
if max_col_value > max_data_value:
|
||||
max_data_value = max_col_value
|
||||
else:
|
||||
print(f"Warning: Column '{col_name}' not found in CSV. Skipping.")
|
||||
|
||||
# --- Dynamic Y-axis Scaling ---
|
||||
ax.set_ylim(bottom=0)
|
||||
if plot_type in scale_config:
|
||||
steps = scale_config[plot_type]['steps']
|
||||
new_max = next((step for step in steps if step >= max_data_value), steps[-1])
|
||||
ax.set_ylim(top=new_max)
|
||||
|
||||
ax.set_title(config['title'])
|
||||
ax.set_ylabel(config['ylabel'])
|
||||
ax.legend()
|
||||
|
||||
# --- Grid and Tick Configuration ---
|
||||
y_min, y_max = ax.get_ylim()
|
||||
|
||||
# Keep the dynamic major_interval logic for tick LABELS
|
||||
if plot_type == 'current' and y_max <= 2.5:
|
||||
major_interval = 0.5
|
||||
elif y_max <= 10:
|
||||
major_interval = 2
|
||||
elif y_max <= 25:
|
||||
major_interval = 5
|
||||
else:
|
||||
major_interval = y_max / 5.0
|
||||
|
||||
ax.yaxis.set_major_locator(MultipleLocator(major_interval))
|
||||
ax.yaxis.set_minor_locator(MultipleLocator(1))
|
||||
|
||||
# Disable the default major grid, but keep the minor one
|
||||
ax.yaxis.grid(False, which='major')
|
||||
ax.yaxis.grid(True, which='minor', linestyle='--', linewidth=0.6, zorder=0)
|
||||
|
||||
# Draw custom lines for 5 and 10 multiples, which are now the only major grid lines
|
||||
for y_val in range(int(y_min), int(y_max) + 1):
|
||||
if y_val == 0: continue
|
||||
if y_val % 10 == 0:
|
||||
ax.axhline(y=y_val, color='maroon', linestyle='--', linewidth=1.2, zorder=1)
|
||||
elif y_val % 5 == 0:
|
||||
ax.axhline(y=y_val, color='midnightblue', linestyle='--', linewidth=1.2, zorder=1)
|
||||
|
||||
# Keep the x-axis grid
|
||||
ax.xaxis.grid(True, which='major', linestyle='--', linewidth=0.8)
|
||||
|
||||
# --- Formatting the x-axis (Time) ---
|
||||
local_tz = gettz()
|
||||
last_ax = axes[-1]
|
||||
|
||||
if not df.empty:
|
||||
last_ax.set_xlim(df['timestamp'].iloc[0], df['timestamp'].iloc[-1])
|
||||
|
||||
last_ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S', tz=local_tz))
|
||||
last_ax.xaxis.set_major_locator(plt.MaxNLocator(15))
|
||||
plt.xlabel(f'Time ({local_tz.tzname(df["timestamp"].iloc[-1])})')
|
||||
plt.xticks(rotation=45)
|
||||
|
||||
# --- Add a main title and subtitle ---
|
||||
start_time = df['timestamp'].iloc[0].strftime('%Y-%m-%d %H:%M:%S')
|
||||
end_time = df['timestamp'].iloc[-1].strftime('%H:%M:%S')
|
||||
main_title = f'PowerMate Log ({start_time} to {end_time})'
|
||||
|
||||
subtitle_parts = []
|
||||
if avg_interval_ms > 0:
|
||||
subtitle_parts.append(f'Avg. Interval: {avg_interval_ms:.2f} ms')
|
||||
|
||||
voltage_strings = [f'{source.upper()} Avg: {avg_v:.2f} V' for source, avg_v in avg_voltages.items()]
|
||||
if voltage_strings:
|
||||
subtitle_parts.extend(voltage_strings)
|
||||
|
||||
subtitle = ' | '.join(subtitle_parts)
|
||||
|
||||
full_title = main_title
|
||||
if subtitle:
|
||||
full_title += f'\n{subtitle}'
|
||||
|
||||
fig.suptitle(full_title, fontsize=14)
|
||||
|
||||
# Adjust layout to make space for the subtitle
|
||||
plt.tight_layout(rect=[0, 0, 1, 0.98])
|
||||
|
||||
# --- Save the plot to a file ---
|
||||
try:
|
||||
plt.savefig(output_path, dpi=150)
|
||||
print(f"Plot successfully saved to '{output_path}'")
|
||||
except Exception as e:
|
||||
print(f"An error occurred while saving the plot: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate a plot from an Odroid PowerMate CSV log file.")
|
||||
parser.add_argument("input_csv", help="Path to the input CSV log file.")
|
||||
parser.add_argument("output_image", help="Path to save the output plot image (e.g., plot.png).")
|
||||
parser.add_argument(
|
||||
"-t", "--type",
|
||||
nargs='+',
|
||||
choices=['power', 'voltage', 'current'],
|
||||
default=['power', 'voltage', 'current'],
|
||||
help="Types of plots to generate. Choose from 'power', 'voltage', 'current'. "
|
||||
"Default is to generate all three."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s", "--source",
|
||||
nargs='+',
|
||||
choices=['vin', 'main', 'usb'],
|
||||
default=['vin', 'main', 'usb'],
|
||||
help="Power sources to plot. Choose from 'vin', 'main', 'usb'. "
|
||||
"Default is to plot all three."
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
plot_power_data(args.input_csv, args.output_image, args.type, args.source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
example/logger/img/plot.png
Normal file
BIN
example/logger/img/plot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
153
example/logger/logger.py
Normal file
153
example/logger/logger.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import csv
|
||||
import requests
|
||||
import websockets
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Import the status_pb2.py file generated by `protoc`.
|
||||
# This file must be in the same directory as logger.py.
|
||||
import status_pb2
|
||||
|
||||
|
||||
class OdroidPowerLogger:
|
||||
"""
|
||||
A class to connect to the Odroid Smart Power monitoring server and log power data.
|
||||
1. Logs into the server via an HTTP POST request to obtain an authentication token.
|
||||
2. Connects to the WebSocket using the obtained token.
|
||||
3. Receives and decodes binary data in Protobuf format, then prints it.
|
||||
"""
|
||||
|
||||
def __init__(self, host, username, password, output_file=None):
|
||||
self.host = host
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.base_url = f"http://{self.host}"
|
||||
self.ws_url = f"ws://{self.host}/ws"
|
||||
self.output_file = output_file
|
||||
self.token = None
|
||||
|
||||
def login(self):
|
||||
"""Logs into the server to retrieve an authentication token."""
|
||||
login_url = f"{self.base_url}/login"
|
||||
payload = {"username": self.username, "password": self.password}
|
||||
try:
|
||||
print(f"Attempting to log in to '{login_url}'...")
|
||||
response = requests.post(login_url, json=payload, timeout=5)
|
||||
response.raise_for_status()
|
||||
|
||||
response_json = response.json()
|
||||
if "token" in response_json:
|
||||
self.token = response_json["token"]
|
||||
print("Login successful! Token received.")
|
||||
return True
|
||||
else:
|
||||
print("Login failed: No token in response.")
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error during login: {e}")
|
||||
return False
|
||||
|
||||
async def listen_power_data(self):
|
||||
"""Connects to the WebSocket to receive and log power data."""
|
||||
if not self.token:
|
||||
print("Cannot connect to WebSocket without an authentication token.")
|
||||
return
|
||||
|
||||
# Add the authentication token as a query parameter
|
||||
uri = f"{self.ws_url}?token={self.token}"
|
||||
|
||||
csv_file = None
|
||||
csv_writer = None
|
||||
|
||||
try:
|
||||
# --- CSV File Handling ---
|
||||
if self.output_file:
|
||||
try:
|
||||
# Open the file in write mode, with newline='' to prevent extra blank rows
|
||||
csv_file = open(self.output_file, 'w', newline='', encoding='utf-8')
|
||||
csv_writer = csv.writer(csv_file)
|
||||
|
||||
# Write header
|
||||
header = [
|
||||
'timestamp', 'uptime_ms',
|
||||
'vin_voltage', 'vin_current', 'vin_power',
|
||||
'main_voltage', 'main_current', 'main_power',
|
||||
'usb_voltage', 'usb_current', 'usb_power'
|
||||
]
|
||||
csv_writer.writerow(header)
|
||||
print(f"Logging data to {self.output_file}")
|
||||
except IOError as e:
|
||||
print(f"Error opening CSV file: {e}")
|
||||
# If file can't be opened, disable CSV writing
|
||||
csv_file = None
|
||||
csv_writer = None
|
||||
# --- End CSV File Handling ---
|
||||
|
||||
async with websockets.connect(uri) as websocket:
|
||||
print(f"Connected to WebSocket: {uri}")
|
||||
while True:
|
||||
# Receive binary message from the server
|
||||
message_bytes = await websocket.recv()
|
||||
|
||||
# Decode the Protobuf message
|
||||
status_message = status_pb2.StatusMessage()
|
||||
status_message.ParseFromString(message_bytes)
|
||||
|
||||
# Process only if the payload type is 'sensor_data'
|
||||
if status_message.WhichOneof('payload') == 'sensor_data':
|
||||
sensor_data = status_message.sensor_data
|
||||
ts_dt = datetime.fromtimestamp(sensor_data.timestamp_ms / 1000, tz=timezone.utc)
|
||||
ts_str_print = ts_dt.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
|
||||
print(f"--- {ts_str_print} (Uptime: {sensor_data.uptime_ms / 1000}s) ---")
|
||||
|
||||
# Print data for each channel
|
||||
for name, channel in [('VIN', sensor_data.vin), ('MAIN', sensor_data.main),
|
||||
('USB', sensor_data.usb)]:
|
||||
print(
|
||||
f" {name:<4}: {channel.voltage:5.2f} V | {channel.current:5.3f} A | {channel.power:5.2f} W")
|
||||
|
||||
# Write to CSV if enabled
|
||||
if csv_writer:
|
||||
ts_iso_csv = ts_dt.isoformat(timespec='milliseconds').replace('+00:00', 'Z')
|
||||
row = [
|
||||
ts_iso_csv, sensor_data.uptime_ms,
|
||||
sensor_data.vin.voltage, sensor_data.vin.current, sensor_data.vin.power,
|
||||
sensor_data.main.voltage, sensor_data.main.current, sensor_data.main.power,
|
||||
sensor_data.usb.voltage, sensor_data.usb.current, sensor_data.usb.power
|
||||
]
|
||||
csv_writer.writerow(row)
|
||||
|
||||
except websockets.exceptions.ConnectionClosed as e:
|
||||
print(f"WebSocket connection closed: {e}")
|
||||
except Exception as e:
|
||||
print(f"Error during WebSocket processing: {e}")
|
||||
finally:
|
||||
if csv_file:
|
||||
csv_file.close()
|
||||
print(f"\nCSV file '{self.output_file}' saved.")
|
||||
|
||||
async def run(self):
|
||||
"""Runs the logger."""
|
||||
if self.login():
|
||||
await self.listen_power_data()
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description="Odroid Smart Power Data Logger")
|
||||
parser.add_argument("host", help="Server's host address or IP (e.g., 192.168.1.10)")
|
||||
parser.add_argument("-u", "--username", required=True, help="Login username")
|
||||
parser.add_argument("-p", "--password", required=True, help="Login password")
|
||||
parser.add_argument("-o", "--output", help="Path to the output CSV file.")
|
||||
args = parser.parse_args()
|
||||
|
||||
logger = OdroidPowerLogger(host=args.host, username=args.username, password=args.password, output_file=args.output)
|
||||
await logger.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\nExiting program.")
|
||||
@@ -2,28 +2,34 @@
|
||||
set(WEB_APP_SOURCE_DIR ${CMAKE_SOURCE_DIR}/page)
|
||||
set(GZ_OUTPUT_FILE ${WEB_APP_SOURCE_DIR}/dist/index.html.gz)
|
||||
|
||||
set(PROTO_DIR ${CMAKE_SOURCE_DIR}/proto)
|
||||
set(PROTO_OUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/proto)
|
||||
set(PROTO_FILE ${PROTO_DIR}/status.proto)
|
||||
set(PROTO_C_FILE ${PROTO_OUT_DIR}/status.pb.c)
|
||||
set(PROTO_H_FILE ${PROTO_OUT_DIR}/status.pb.h)
|
||||
|
||||
# Check npm is available
|
||||
find_program(NPM_EXECUTABLE npm)
|
||||
if(NOT NPM_EXECUTABLE)
|
||||
if (NOT NPM_EXECUTABLE)
|
||||
message(FATAL_ERROR "npm not found! Please install Node.js and npm.")
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
# Register the component. Now, CMake knows how GZ_OUTPUT_FILE is generated
|
||||
# and can correctly handle the dependency for embedding.
|
||||
idf_component_register(SRC_DIRS "app" "nconfig" "wifi" "indicator" "system" "service" "ina226"
|
||||
INCLUDE_DIRS "include"
|
||||
idf_component_register(SRC_DIRS "app" "nconfig" "wifi" "indicator" "service" "proto"
|
||||
INCLUDE_DIRS "include" "proto"
|
||||
EMBED_FILES ${GZ_OUTPUT_FILE}
|
||||
)
|
||||
|
||||
# Define a custom command to build the web app.
|
||||
# This command explicitly tells CMake that it produces the GZ_OUTPUT_FILE.
|
||||
add_custom_command(
|
||||
OUTPUT ${GZ_OUTPUT_FILE}
|
||||
COMMAND npm install
|
||||
COMMAND npm run build
|
||||
WORKING_DIRECTORY ${WEB_APP_SOURCE_DIR}
|
||||
# Re-run the build if any of these files change
|
||||
DEPENDS
|
||||
OUTPUT ${GZ_OUTPUT_FILE}
|
||||
COMMAND npm install
|
||||
COMMAND npm run build
|
||||
WORKING_DIRECTORY ${WEB_APP_SOURCE_DIR}
|
||||
# Re-run the build if any of these files change
|
||||
DEPENDS
|
||||
${WEB_APP_SOURCE_DIR}/package.json
|
||||
${WEB_APP_SOURCE_DIR}/vite.config.js
|
||||
${WEB_APP_SOURCE_DIR}/index.html
|
||||
@@ -38,14 +44,46 @@ add_custom_command(
|
||||
${WEB_APP_SOURCE_DIR}/src/utils.js
|
||||
${WEB_APP_SOURCE_DIR}/src/websocket.js
|
||||
|
||||
COMMENT "Building Node.js project (npm install && npm run build)"
|
||||
VERBATIM
|
||||
COMMENT "Building Node.js project (npm install && npm run build)"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
# Create a target that depends on the output file. When this target is built,
|
||||
# it ensures the custom command above is executed first.
|
||||
add_custom_target(build_web_app ALL
|
||||
DEPENDS ${GZ_OUTPUT_FILE}
|
||||
DEPENDS ${GZ_OUTPUT_FILE}
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${PROTO_C_FILE} ${PROTO_H_FILE}
|
||||
COMMAND protoc --nanopb_out=${PROTO_OUT_DIR} status.proto
|
||||
WORKING_DIRECTORY ${PROTO_DIR}
|
||||
DEPENDS ${PROTO_FILE}
|
||||
COMMENT "Generating C sources from ${PROTO_FILE} using nanopb"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(protobuf_generate ALL
|
||||
DEPENDS ${PROTO_C_FILE} ${PROTO_H_FILE}
|
||||
)
|
||||
|
||||
add_dependencies(${COMPONENT_LIB} build_web_app)
|
||||
add_dependencies(${COMPONENT_LIB} protobuf_generate)
|
||||
|
||||
execute_process(
|
||||
COMMAND git rev-parse --short HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
add_compile_definitions(VERSION_HASH="${GIT_HASH}")
|
||||
|
||||
execute_process(
|
||||
COMMAND git describe --tags --abbrev=0
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_TAG
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
add_compile_definitions(VERSION_TAG="${GIT_TAG}")
|
||||
90
main/Kconfig
90
main/Kconfig
@@ -2,27 +2,34 @@ menu "ODROID-MONITOR"
|
||||
menu "GPIO"
|
||||
orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps"
|
||||
|
||||
config GPIO_INA226_SCL
|
||||
config I2C_GPIO_SCL
|
||||
int "INA226 SCL GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 0
|
||||
help
|
||||
GPIO number for I2C Master data line.
|
||||
|
||||
config GPIO_INA226_SDA
|
||||
int "INA226 SDA GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 1
|
||||
help
|
||||
GPIO number for I2C Master data line.
|
||||
|
||||
config GPIO_INA226_INT
|
||||
int "INA226 ALERT GPIO Num"
|
||||
config I2C_GPIO_SDA
|
||||
int "INA226 SDA GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 10
|
||||
default 0
|
||||
help
|
||||
GPIO number for I2C Master data line.
|
||||
|
||||
config GPIO_INA3221_INT_CRITICAL
|
||||
int "INA226 ALERT GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 9
|
||||
help
|
||||
GPIO number for critical int pin.
|
||||
|
||||
config GPIO_INA3221_INT_WARNING
|
||||
int "INA226 WARNING GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 5
|
||||
help
|
||||
GPIO number for critical int pin.
|
||||
|
||||
config GPIO_UART_TX
|
||||
int "UART TX GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
@@ -40,43 +47,64 @@ menu "ODROID-MONITOR"
|
||||
config GPIO_LED_STATUS
|
||||
int "Status LED GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 8
|
||||
default 2
|
||||
help
|
||||
GPIO number for LED.
|
||||
|
||||
config GPIO_LED_WIFI
|
||||
int "Wi-Fi LED GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 9
|
||||
default 3
|
||||
help
|
||||
GPIO number for LED.
|
||||
|
||||
config GPIO_SW_12V
|
||||
config GPIO_EXPANDER_RESET
|
||||
int "Trigger reset GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 8
|
||||
help
|
||||
GPIO number for Reset expander.
|
||||
|
||||
config EXPANDER_GPIO_SW_12V
|
||||
int "12v Load Switch GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 4
|
||||
help
|
||||
GPIO number for Load switch.
|
||||
|
||||
config GPIO_SW_5V
|
||||
int "5v Load Switch GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 5
|
||||
help
|
||||
GPIO number for Load switch.
|
||||
|
||||
config GPIO_TRIGGER_POWER
|
||||
int "Trigger power GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 2
|
||||
help
|
||||
GPIO number for Trigger.
|
||||
GPIO number for Load switch.
|
||||
|
||||
config GPIO_TRIGGER_RESET
|
||||
int "Trigger reset GPIO Num"
|
||||
config EXPANDER_GPIO_SW_5V
|
||||
int "5v Load Switch GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 3
|
||||
help
|
||||
GPIO number for Load switch.
|
||||
|
||||
config EXPANDER_GPIO_TRIGGER_POWER
|
||||
int "Trigger power GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 0
|
||||
help
|
||||
GPIO number for Trigger.
|
||||
|
||||
config EXPANDER_GPIO_TRIGGER_RESET
|
||||
int "Trigger reset GPIO Num"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 1
|
||||
help
|
||||
GPIO number for Trigger.
|
||||
|
||||
config TRIGGER_POWER_DELAY_MS
|
||||
int "Trigger reset GPIO Num"
|
||||
range 100 5000
|
||||
default 3000
|
||||
help
|
||||
Reset delay ms.
|
||||
|
||||
config TRIGGER_RESET_DELAY_MS
|
||||
int "Trigger reset GPIO Num"
|
||||
range 100 5000
|
||||
default 1000
|
||||
help
|
||||
Reset delay ms.
|
||||
endmenu
|
||||
endmenu
|
||||
@@ -1,50 +1,44 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/param.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_netif.h"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "i2cdev.h"
|
||||
#include "indicator.h"
|
||||
#include "nconfig.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "system.h"
|
||||
#include "wifi.h"
|
||||
#include "storage.h"
|
||||
|
||||
#include "lwip/err.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/sys.h"
|
||||
void app_main(void)
|
||||
{
|
||||
printf("\n\n== ODROID POWER-MATE ===\n");
|
||||
printf("Version: %s-%s\n\n", VERSION_TAG, VERSION_HASH);
|
||||
|
||||
static const char *TAG = "odroid-remote";
|
||||
|
||||
void app_main(void) {
|
||||
ESP_ERROR_CHECK(i2cdev_init());
|
||||
init_led();
|
||||
led_set(LED_BLU, BLINK_TRIPLE);
|
||||
led_off(LED_BLU);
|
||||
|
||||
// NVS 초기화
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
|
||||
{
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
// 네트워크 초기화
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
ESP_ERROR_CHECK(init_nconfig());
|
||||
|
||||
// WiFi 연결
|
||||
wifi_init();
|
||||
wifi_connect();
|
||||
sync_time();
|
||||
|
||||
start_webserver();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
dependencies:
|
||||
espressif/led_indicator: ^1.1.1
|
||||
joltwallet/littlefs: ==1.20.1
|
||||
esp-idf-lib/ina3221: ^1.1.7
|
||||
esp-idf-lib/pca9557: ^1.0.7
|
||||
nikas-belogolov/nanopb: ^1.0.0
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
#include "ina226.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
#define INA226_REG_CONFIG (0x00)
|
||||
#define INA226_REG_SHUNT_VOLTAGE (0x01)
|
||||
#define INA226_REG_BUS_VOLTAGE (0x02)
|
||||
#define INA226_REG_POWER (0x03)
|
||||
#define INA226_REG_CURRENT (0x04)
|
||||
#define INA226_REG_CALIBRATION (0x05)
|
||||
#define INA226_REG_ALERT_MASK (0x06)
|
||||
#define INA226_REG_ALERT_LIMIT (0x07)
|
||||
#define INA226_REG_MANUFACTURER_ID (0xFE)
|
||||
#define INA226_REG_DIE_ID (0xFF)
|
||||
|
||||
#define INA226_CFG_AVERAGING_OFFSET 9
|
||||
#define INA226_CFG_BUS_VOLTAGE_OFFSET 6
|
||||
#define INA226_CFG_SHUNT_VOLTAGE_OFFSET 3
|
||||
|
||||
static esp_err_t ina226_read_reg(ina226_t *handle, uint8_t reg_addr, uint16_t *data, size_t len)
|
||||
{
|
||||
return i2c_master_transmit_receive(handle->dev_handle, ®_addr, 1, (uint8_t *)data, len, handle->timeout_ms);
|
||||
}
|
||||
|
||||
static esp_err_t ina226_write_reg(ina226_t *handle, uint8_t reg_addr, uint16_t value)
|
||||
{
|
||||
uint8_t write_buf[3] = {reg_addr, value >> 8, value & 0xFF};
|
||||
return i2c_master_transmit(handle->dev_handle, write_buf, sizeof(write_buf), handle->timeout_ms);
|
||||
}
|
||||
|
||||
esp_err_t ina226_get_manufacturer_id(ina226_t *device, uint16_t *manufacturer_id)
|
||||
{
|
||||
return ina226_read_reg(device, INA226_REG_MANUFACTURER_ID, manufacturer_id, 2);
|
||||
}
|
||||
|
||||
esp_err_t ina226_get_die_id(ina226_t *device, uint16_t *die_id)
|
||||
{
|
||||
return ina226_read_reg(device, INA226_REG_DIE_ID, die_id, 2);
|
||||
}
|
||||
|
||||
esp_err_t ina226_get_shunt_voltage(ina226_t *device, float *voltage)
|
||||
{
|
||||
uint8_t data[2];
|
||||
esp_err_t err = ina226_read_reg(device, INA226_REG_SHUNT_VOLTAGE, (uint16_t*)data, 2);
|
||||
*voltage = (float) (data[0] << 8 | data[1]) * 2.5e-6f; /* fixed to 2.5 uV */
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t ina226_get_bus_voltage(ina226_t *device, float *voltage)
|
||||
{
|
||||
uint8_t data[2];
|
||||
esp_err_t err = ina226_read_reg(device, INA226_REG_BUS_VOLTAGE, (uint16_t*)data, 2);
|
||||
*voltage = (float) (data[0] << 8 | data[1]) * 0.00125f;
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t ina226_get_current(ina226_t *device, float *current)
|
||||
{
|
||||
uint8_t data[2];
|
||||
esp_err_t err = ina226_read_reg(device, INA226_REG_CURRENT, (uint16_t*)data, 2);
|
||||
*current = ((float) (data[0] << 8 | data[1])) * device->current_lsb;
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t ina226_get_power(ina226_t *device, float *power)
|
||||
{
|
||||
uint8_t data[2];
|
||||
esp_err_t err = ina226_read_reg(device, INA226_REG_POWER, (uint16_t*)data, 2);
|
||||
*power = (float) (data[0] << 8 | data[1]) * device->power_lsb;
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t ina226_init(ina226_t *device, i2c_master_dev_handle_t dev_handle, const ina226_config_t *config)
|
||||
{
|
||||
esp_err_t err;
|
||||
device->timeout_ms = config->timeout_ms;
|
||||
device->dev_handle = dev_handle;
|
||||
uint16_t bitmask = 0;
|
||||
bitmask |= (config->averages << INA226_CFG_AVERAGING_OFFSET);
|
||||
bitmask |= (config->bus_conv_time << INA226_CFG_BUS_VOLTAGE_OFFSET);
|
||||
bitmask |= (config->shunt_conv_time << INA226_CFG_SHUNT_VOLTAGE_OFFSET);
|
||||
bitmask |= config->mode;
|
||||
err = ina226_write_reg(device, INA226_REG_CONFIG, bitmask);
|
||||
if(err != ESP_OK) return err;
|
||||
|
||||
/* write calibration*/
|
||||
float minimum_lsb = config->max_current / 32767;
|
||||
float current_lsb = (uint16_t)(minimum_lsb * 100000000);
|
||||
current_lsb /= 100000000;
|
||||
current_lsb /= 0.0001;
|
||||
current_lsb = ceil(current_lsb);
|
||||
current_lsb *= 0.0001;
|
||||
device->current_lsb = current_lsb;
|
||||
device->power_lsb = current_lsb * 25;
|
||||
uint16_t calibration_value = (uint16_t)((0.00512) / (current_lsb * config->r_shunt));
|
||||
err = ina226_write_reg(device, INA226_REG_CALIBRATION, calibration_value);
|
||||
if(err != ESP_OK) return err;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ina226_get_alert_mask(ina226_t *device, ina226_alert_t *alert_mask)
|
||||
{
|
||||
return ina226_read_reg(device, INA226_REG_ALERT_MASK, (uint16_t *)alert_mask, 2);
|
||||
}
|
||||
|
||||
esp_err_t ina226_set_alert_mask(ina226_t *device, ina226_alert_t alert_mask)
|
||||
{
|
||||
return ina226_write_reg(device, INA226_REG_ALERT_MASK, (uint16_t)alert_mask);
|
||||
}
|
||||
|
||||
esp_err_t ina226_set_alert_limit(ina226_t *device, float voltage)
|
||||
{
|
||||
return ina226_write_reg(device, INA226_REG_ALERT_LIMIT, (uint16_t)(voltage));
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
#ifndef _INA226_H_
|
||||
#define _INA226_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
#include "driver/i2c_master.h"
|
||||
|
||||
typedef enum
|
||||
{
|
||||
INA226_AVERAGES_1 = 0b000,
|
||||
INA226_AVERAGES_4 = 0b001,
|
||||
INA226_AVERAGES_16 = 0b010,
|
||||
INA226_AVERAGES_64 = 0b011,
|
||||
INA226_AVERAGES_128 = 0b100,
|
||||
INA226_AVERAGES_256 = 0b101,
|
||||
INA226_AVERAGES_512 = 0b110,
|
||||
INA226_AVERAGES_1024 = 0b111
|
||||
} ina226_averages_t;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
INA226_BUS_CONV_TIME_140_US = 0b000,
|
||||
INA226_BUS_CONV_TIME_204_US = 0b001,
|
||||
INA226_BUS_CONV_TIME_332_US = 0b010,
|
||||
INA226_BUS_CONV_TIME_588_US = 0b011,
|
||||
INA226_BUS_CONV_TIME_1100_US = 0b100,
|
||||
INA226_BUS_CONV_TIME_2116_US = 0b101,
|
||||
INA226_BUS_CONV_TIME_4156_US = 0b110,
|
||||
INA226_BUS_CONV_TIME_8244_US = 0b111
|
||||
} ina226_bus_conv_time_t;
|
||||
|
||||
|
||||
typedef enum
|
||||
{
|
||||
INA226_SHUNT_CONV_TIME_140_US = 0b000,
|
||||
INA226_SHUNT_CONV_TIME_204_US = 0b001,
|
||||
INA226_SHUNT_CONV_TIME_332_US = 0b010,
|
||||
INA226_SHUNT_CONV_TIME_588_US = 0b011,
|
||||
INA226_SHUNT_CONV_TIME_1100_US = 0b100,
|
||||
INA226_SHUNT_CONV_TIME_2116_US = 0b101,
|
||||
INA226_SHUNT_CONV_TIME_4156_US = 0b110,
|
||||
INA226_SHUNT_CONV_TIME_8244_US = 0b111
|
||||
} ina226_shunt_conv_time_t;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
|
||||
INA226_MODE_POWER_DOWN = 0b000,
|
||||
INA226_MODE_SHUNT_TRIG = 0b001,
|
||||
INA226_MODE_BUS_TRIG = 0b010,
|
||||
INA226_MODE_SHUNT_BUS_TRIG = 0b011,
|
||||
INA226_MODE_ADC_OFF = 0b100,
|
||||
INA226_MODE_SHUNT_CONT = 0b101,
|
||||
INA226_MODE_BUS_CONT = 0b110,
|
||||
INA226_MODE_SHUNT_BUS_CONT = 0b111,
|
||||
} ina226_mode_t;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
INA226_ALERT_SHUNT_OVER_VOLTAGE = 0xf,
|
||||
INA226_ALERT_SHUNT_UNDER_VOLTAGE = 0xe,
|
||||
INA226_ALERT_BUS_OVER_VOLTAGE = 0xd,
|
||||
INA226_ALERT_BUS_UNDER_VOLTAGE = 0xc,
|
||||
INA226_ALERT_POWER_OVER_LIMIT = 0xb,
|
||||
INA226_ALERT_CONVERSION_READY = 0xa,
|
||||
INA226_ALERT_FUNCTION_FLAG = 0x4,
|
||||
INA226_ALERT_CONVERSION_READY_FLAG = 0x3,
|
||||
INA226_ALERT_MATH_OVERFLOW_FLAG = 0x2,
|
||||
INA226_ALERT_POLARITY = 0x1,
|
||||
INA226_ALERT_LATCH_ENABLE = 0x0
|
||||
} ina226_alert_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
i2c_port_t i2c_port;
|
||||
int i2c_addr;
|
||||
int timeout_ms;
|
||||
ina226_averages_t averages;
|
||||
ina226_bus_conv_time_t bus_conv_time;
|
||||
ina226_shunt_conv_time_t shunt_conv_time;
|
||||
ina226_mode_t mode;
|
||||
float r_shunt; /* ohm */
|
||||
float max_current; /* amps */
|
||||
} ina226_config_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
i2c_master_dev_handle_t dev_handle;
|
||||
int timeout_ms;
|
||||
float current_lsb;
|
||||
float power_lsb;
|
||||
} ina226_t;
|
||||
|
||||
esp_err_t ina226_get_manufacturer_id(ina226_t *device, uint16_t *manufacturer_id);
|
||||
esp_err_t ina226_get_die_id(ina226_t *device, uint16_t *die_id);
|
||||
esp_err_t ina226_get_shunt_voltage(ina226_t *device, float *voltage);
|
||||
esp_err_t ina226_get_bus_voltage(ina226_t *device, float *voltage);
|
||||
esp_err_t ina226_get_current(ina226_t *device, float *current);
|
||||
esp_err_t ina226_get_power(ina226_t *device, float *power);
|
||||
esp_err_t ina226_get_alert_mask(ina226_t *device, ina226_alert_t *alert_mask);
|
||||
esp_err_t ina226_set_alert_mask(ina226_t *device, ina226_alert_t alert_mask);
|
||||
esp_err_t ina226_set_alert_limit(ina226_t *device, float voltage);
|
||||
esp_err_t ina226_init(ina226_t *device, i2c_master_dev_handle_t dev_handle, const ina226_config_t *config);
|
||||
|
||||
#endif
|
||||
@@ -5,25 +5,50 @@
|
||||
#ifndef LED_H
|
||||
#define LED_H
|
||||
|
||||
/**
|
||||
* @brief Defines the different blinking patterns for the LEDs.
|
||||
*/
|
||||
enum blink_type
|
||||
{
|
||||
BLINK_SLOW = 0,
|
||||
BLINK_FAST,
|
||||
BLINK_DOUBLE,
|
||||
BLINK_TRIPLE,
|
||||
BLINK_SOLID,
|
||||
BLINK_MAX,
|
||||
BLINK_SLOW = 0, ///< Slow blinking pattern.
|
||||
BLINK_FAST, ///< Fast blinking pattern.
|
||||
BLINK_DOUBLE, ///< Double blink pattern.
|
||||
BLINK_TRIPLE, ///< Triple blink pattern.
|
||||
BLINK_SOLID, ///< Solid (always on) state.
|
||||
BLINK_MAX, ///< Sentinel for the number of blink types.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Defines the available LEDs that can be controlled.
|
||||
*/
|
||||
enum blink_led
|
||||
{
|
||||
LED_RED = 0,
|
||||
LED_BLU = 1,
|
||||
LED_MAX,
|
||||
LED_RED = 0, ///< The red LED.
|
||||
LED_BLU = 1, ///< The blue LED.
|
||||
LED_MAX, ///< Sentinel for the number of LEDs.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Initializes the LED indicator system.
|
||||
*
|
||||
* This function sets up the GPIOs and timers required for the LED blinking patterns.
|
||||
* It should be called once during application startup.
|
||||
*/
|
||||
void init_led(void);
|
||||
|
||||
/**
|
||||
* @brief Sets a specific blinking pattern for an LED.
|
||||
*
|
||||
* @param led The LED to control (e.g., LED_RED, LED_BLU).
|
||||
* @param type The blinking pattern to apply (e.g., BLINK_FAST, BLINK_SOLID).
|
||||
*/
|
||||
void led_set(enum blink_led led, enum blink_type type);
|
||||
|
||||
/**
|
||||
* @brief Turns off a specific LED.
|
||||
*
|
||||
* @param led The LED to turn off.
|
||||
*/
|
||||
void led_off(enum blink_led led);
|
||||
|
||||
#endif //LED_H
|
||||
#endif // LED_H
|
||||
|
||||
@@ -1,46 +1,100 @@
|
||||
//
|
||||
// Created by shinys on 25. 7. 10.
|
||||
//
|
||||
/**
|
||||
* @file nconfig.h
|
||||
* @brief Provides an interface for managing system configuration using Non-Volatile Storage (NVS).
|
||||
*/
|
||||
|
||||
#ifndef NCONFIG_H
|
||||
#define NCONFIG_H
|
||||
|
||||
#include "nvs.h"
|
||||
#include "esp_err.h"
|
||||
#include "nvs.h"
|
||||
|
||||
#define NCONFIG_NVS_NAMESPACE "er"
|
||||
#define NCONFIG_NOT_FOUND ESP_ERR_NVS_NOT_FOUND
|
||||
#define NCONFIG_NVS_NAMESPACE "er" ///< The NVS namespace where all configurations are stored.
|
||||
#define NCONFIG_NOT_FOUND ESP_ERR_NVS_NOT_FOUND ///< Alias for the NVS not found error code.
|
||||
|
||||
/**
|
||||
* @brief Initializes the nconfig module.
|
||||
*
|
||||
* This function initializes the Non-Volatile Storage (NVS) component, which is required
|
||||
* for all other nconfig operations. It should be called once at application startup.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t init_nconfig();
|
||||
|
||||
/**
|
||||
* @brief Defines the keys for all configuration values managed by nconfig.
|
||||
*/
|
||||
enum nconfig_type
|
||||
{
|
||||
WIFI_SSID,
|
||||
WIFI_PASSWORD,
|
||||
WIFI_MODE,
|
||||
AP_SSID,
|
||||
AP_PASSWORD,
|
||||
NETIF_HOSTNAME,
|
||||
NETIF_IP,
|
||||
NETIF_GATEWAY,
|
||||
NETIF_SUBNET,
|
||||
NETIF_DNS1,
|
||||
NETIF_DNS2,
|
||||
NETIF_TYPE,
|
||||
UART_BAUD_RATE,
|
||||
NCONFIG_TYPE_MAX,
|
||||
WIFI_SSID, ///< The SSID of the Wi-Fi network to connect to (STA mode).
|
||||
WIFI_PASSWORD, ///< The password for the Wi-Fi network (STA mode).
|
||||
WIFI_MODE, ///< The Wi-Fi operating mode (e.g., "sta" or "apsta").
|
||||
AP_SSID, ///< The SSID for the device's Access Point mode.
|
||||
AP_PASSWORD, ///< The password for the device's Access Point mode.
|
||||
NETIF_HOSTNAME, ///< The hostname of the device on the network.
|
||||
NETIF_IP, ///< The static IP address for the STA interface.
|
||||
NETIF_GATEWAY, ///< The gateway address for the STA interface.
|
||||
NETIF_SUBNET, ///< The subnet mask for the STA interface.
|
||||
NETIF_DNS1, ///< The primary DNS server address.
|
||||
NETIF_DNS2, ///< The secondary DNS server address.
|
||||
NETIF_TYPE, ///< The network interface type (e.g., "dhcp" or "static").
|
||||
UART_BAUD_RATE, ///< The baud rate for the UART communication.
|
||||
VIN_CURRENT_LIMIT, ///< The maximum current limit for the VIN.
|
||||
MAIN_CURRENT_LIMIT, ///< The maximum current limit for the MAIN out.
|
||||
USB_CURRENT_LIMIT, ///< The maximum current limit for the USB out.
|
||||
PAGE_USERNAME, ///< Webpage username
|
||||
PAGE_PASSWORD, ///< Webpage password
|
||||
SENSOR_PERIOD_MS, ///< Sensor period
|
||||
NCONFIG_TYPE_MAX, ///< Sentinel for the maximum number of configuration types.
|
||||
};
|
||||
|
||||
// Write config
|
||||
/**
|
||||
* @brief Erase all of nvs data and restart system
|
||||
*/
|
||||
void reset_nconfig();
|
||||
|
||||
/**
|
||||
* @brief Checks if a specific configuration value has been set.
|
||||
*
|
||||
* @param type The configuration key to check.
|
||||
* @return True if the value is not set, false otherwise.
|
||||
*/
|
||||
bool nconfig_value_is_not_set(enum nconfig_type type);
|
||||
|
||||
/**
|
||||
* @brief Writes a configuration value to NVS.
|
||||
*
|
||||
* @param type The configuration key to write.
|
||||
* @param data A pointer to the string data to be stored.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t nconfig_write(enum nconfig_type type, const char* data);
|
||||
|
||||
// Check config is set and get config value length
|
||||
esp_err_t nconfig_get_str_len(enum nconfig_type type, size_t *len);
|
||||
/**
|
||||
* @brief Gets the length of a stored string configuration value.
|
||||
*
|
||||
* @param type The configuration key to query.
|
||||
* @param[out] len A pointer to a size_t variable to store the length.
|
||||
* @return ESP_OK on success, NCONFIG_NOT_FOUND if the key doesn't exist, or another error code.
|
||||
*/
|
||||
esp_err_t nconfig_get_str_len(enum nconfig_type type, size_t* len);
|
||||
|
||||
// Read config
|
||||
/**
|
||||
* @brief Reads a configuration value from NVS.
|
||||
*
|
||||
* @param type The configuration key to read.
|
||||
* @param[out] data A buffer to store the read data.
|
||||
* @param len The length of the buffer. It should be large enough to hold the value.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t nconfig_read(enum nconfig_type type, char* data, size_t len);
|
||||
|
||||
// Remove key
|
||||
/**
|
||||
* @brief Deletes a configuration key-value pair from NVS.
|
||||
*
|
||||
* @param type The configuration key to delete.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t nconfig_delete(enum nconfig_type type);
|
||||
|
||||
#endif //NCONFIG_H
|
||||
#endif // NCONFIG_H
|
||||
|
||||
6
main/include/storage.h
Normal file
6
main/include/storage.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#ifndef STORAGE_H_
|
||||
#define STORAGE_H_
|
||||
|
||||
void storage_init(void);
|
||||
|
||||
#endif /* STORAGE_H_ */
|
||||
@@ -1,12 +1,33 @@
|
||||
//
|
||||
// Created by shinys on 25. 8. 5.
|
||||
//
|
||||
/**
|
||||
* @file system.h
|
||||
* @brief Declares system-level functions for core operations like rebooting and starting services.
|
||||
*/
|
||||
|
||||
#ifndef SYSTEM_H
|
||||
#define SYSTEM_H
|
||||
|
||||
/**
|
||||
* @brief Starts a timer that will reboot the system after a specified duration.
|
||||
*
|
||||
* This function is non-blocking. It schedules a system reboot to occur in the future.
|
||||
* @param sec The number of seconds to wait before rebooting.
|
||||
*/
|
||||
void start_reboot_timer(int sec);
|
||||
void stop_reboot_timer();
|
||||
void start_webserver();
|
||||
|
||||
#endif //SYSTEM_H
|
||||
/**
|
||||
* @brief Stops any pending reboot timer that was previously started.
|
||||
*
|
||||
* If a reboot timer is active, this function will cancel it, preventing the system from rebooting.
|
||||
*/
|
||||
void stop_reboot_timer();
|
||||
|
||||
/**
|
||||
* @brief Initializes and starts the main web server.
|
||||
*
|
||||
* This function sets up the HTTP server, registers all URI handlers for web pages,
|
||||
* API endpoints (like control and settings), and the WebSocket endpoint. It also
|
||||
* initializes the status monitor that provides real-time data.
|
||||
*/
|
||||
void start_webserver(void);
|
||||
|
||||
#endif // SYSTEM_H
|
||||
|
||||
@@ -8,16 +8,106 @@
|
||||
#include "esp_netif_types.h"
|
||||
#include "esp_wifi_types_generic.h"
|
||||
|
||||
/**
|
||||
* @brief Initializes the Wi-Fi driver, network interfaces, and event handlers.
|
||||
*/
|
||||
void wifi_init(void);
|
||||
|
||||
/**
|
||||
* @brief Converts a Wi-Fi authentication mode enum to its string representation.
|
||||
* @param mode The Wi-Fi authentication mode.
|
||||
* @return A string describing the authentication mode.
|
||||
*/
|
||||
const char* auth_mode_str(wifi_auth_mode_t mode);
|
||||
|
||||
/**
|
||||
* @brief Converts a Wi-Fi disconnection reason enum to its string representation.
|
||||
* @param reason The reason for disconnection.
|
||||
* @return A string describing the reason.
|
||||
*/
|
||||
const char* wifi_reason_str(wifi_err_reason_t reason);
|
||||
|
||||
/**
|
||||
* @brief Connects the device to the configured Wi-Fi AP in STA mode.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t wifi_connect(void);
|
||||
|
||||
/**
|
||||
* @brief Disconnects the device from the current Wi-Fi AP in STA mode.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t wifi_disconnect(void);
|
||||
void wifi_scan_aps(wifi_ap_record_t **ap_records, uint16_t* count);
|
||||
esp_err_t wifi_get_current_ap_info(wifi_ap_record_t *ap_info);
|
||||
esp_err_t wifi_get_current_ip_info(esp_netif_ip_info_t *ip_info);
|
||||
esp_err_t wifi_get_dns_info(esp_netif_dns_type_t type, esp_netif_dns_info_t *dns_info);
|
||||
|
||||
/**
|
||||
* @brief Scans for available Wi-Fi access points.
|
||||
* @param ap_records A pointer to store the found AP records.
|
||||
* @param count A pointer to store the number of found APs.
|
||||
*/
|
||||
void wifi_scan_aps(wifi_ap_record_t** ap_records, uint16_t* count);
|
||||
|
||||
/**
|
||||
* @brief Gets information about the currently connected access point.
|
||||
* @param ap_info A pointer to a structure to store the AP information.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t wifi_get_current_ap_info(wifi_ap_record_t* ap_info);
|
||||
|
||||
/**
|
||||
* @brief Gets the current IP information for the STA interface.
|
||||
* @param ip_info A pointer to a structure to store the IP information.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t wifi_get_current_ip_info(esp_netif_ip_info_t* ip_info);
|
||||
|
||||
/**
|
||||
* @brief Gets the DNS server information for the STA interface.
|
||||
* @param type The type of DNS server (main, backup, fallback).
|
||||
* @param dns_info A pointer to a structure to store the DNS information.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t wifi_get_dns_info(esp_netif_dns_type_t type, esp_netif_dns_info_t* dns_info);
|
||||
|
||||
/**
|
||||
* @brief Configures the STA interface to use DHCP.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t wifi_use_dhcp(void);
|
||||
esp_err_t wifi_use_static(const char *ip, const char *gw, const char *netmask, const char *dns1, const char *dns2);
|
||||
|
||||
/**
|
||||
* @brief Configures the STA interface to use a static IP address.
|
||||
* @param ip The static IP address.
|
||||
* @param gw The gateway address.
|
||||
* @param netmask The subnet mask.
|
||||
* @param dns1 The primary DNS server.
|
||||
* @param dns2 The secondary DNS server (optional).
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t wifi_use_static(const char* ip, const char* gw, const char* netmask, const char* dns1, const char* dns2);
|
||||
|
||||
/**
|
||||
* @brief Switches the Wi-Fi operating mode (e.g., sta, apsta).
|
||||
* @param mode The target Wi-Fi mode as a string ("sta" or "apsta").
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t wifi_switch_mode(const char* mode);
|
||||
|
||||
/**
|
||||
* @brief Initializes the SNTP service for time synchronization.
|
||||
*/
|
||||
void initialize_sntp(void);
|
||||
|
||||
/**
|
||||
* @brief Starts the SNTP time synchronization process.
|
||||
*/
|
||||
void sync_time();
|
||||
|
||||
#endif //WIFI_H
|
||||
/**
|
||||
* @brief Sets the SSID and password for the STA mode, saves them to NVS, and connects to the AP.
|
||||
* @param ssid The SSID of the access point.
|
||||
* @param password The password of the access point.
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t wifi_sta_set_ap(const char* ssid, const char* password);
|
||||
|
||||
#endif // WIFI_H
|
||||
|
||||
@@ -48,13 +48,9 @@ static const blink_step_t solid_blink[] = {
|
||||
{LED_BLINK_LOOP, 0, 0},
|
||||
};
|
||||
|
||||
blink_step_t const *led_mode[] = {
|
||||
[BLINK_SLOW] = slow_blink,
|
||||
[BLINK_FAST] = fast_blink,
|
||||
[BLINK_DOUBLE] = double_blink,
|
||||
[BLINK_TRIPLE] = triple_blink,
|
||||
[BLINK_SOLID] = solid_blink,
|
||||
[BLINK_MAX] = NULL,
|
||||
blink_step_t const* led_mode[] = {
|
||||
[BLINK_SLOW] = slow_blink, [BLINK_FAST] = fast_blink, [BLINK_DOUBLE] = double_blink,
|
||||
[BLINK_TRIPLE] = triple_blink, [BLINK_SOLID] = solid_blink, [BLINK_MAX] = NULL,
|
||||
};
|
||||
|
||||
led_indicator_handle_t led_handle[LED_MAX] = {0};
|
||||
@@ -65,7 +61,7 @@ void init_led(void)
|
||||
led_indicator_ledc_config_t ledc_config = {0};
|
||||
led_indicator_config_t config = {0};
|
||||
|
||||
ledc_config.is_active_level_high = true;
|
||||
ledc_config.is_active_level_high = false;
|
||||
ledc_config.timer_inited = false;
|
||||
ledc_config.timer_num = LEDC_TIMER_0;
|
||||
ledc_config.gpio_num = LED_STATUS_GPIO;
|
||||
@@ -78,7 +74,7 @@ void init_led(void)
|
||||
|
||||
led_handle[LED_RED] = led_indicator_create(&config);
|
||||
|
||||
ledc_config.is_active_level_high = true;
|
||||
ledc_config.is_active_level_high = false;
|
||||
ledc_config.timer_inited = false;
|
||||
ledc_config.timer_num = LEDC_TIMER_0;
|
||||
ledc_config.gpio_num = LED_WIFI_GPIO;
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
|
||||
#include "nconfig.h"
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "indicator.h"
|
||||
#include "system.h"
|
||||
#include "esp_err.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
static nvs_handle_t handle;
|
||||
|
||||
const static char *keys[NCONFIG_TYPE_MAX] = {
|
||||
const static char* keys[NCONFIG_TYPE_MAX] = {
|
||||
[WIFI_SSID] = "wifi_ssid",
|
||||
[WIFI_PASSWORD] = "wifi_pw",
|
||||
[WIFI_MODE] = "wifi_mode",
|
||||
@@ -23,15 +25,22 @@ const static char *keys[NCONFIG_TYPE_MAX] = {
|
||||
[NETIF_DNS2] = "dns2",
|
||||
[NETIF_TYPE] = "dhcp",
|
||||
[UART_BAUD_RATE] = "baudrate",
|
||||
[VIN_CURRENT_LIMIT] = "vin_climit",
|
||||
[MAIN_CURRENT_LIMIT] = "main_climit",
|
||||
[USB_CURRENT_LIMIT] = "usb_climit",
|
||||
[PAGE_USERNAME] = "username",
|
||||
[PAGE_PASSWORD] = "password",
|
||||
[SENSOR_PERIOD_MS] = "sensor_period",
|
||||
};
|
||||
|
||||
struct default_value {
|
||||
struct default_value
|
||||
{
|
||||
enum nconfig_type type;
|
||||
const char *value;
|
||||
const char* value;
|
||||
};
|
||||
|
||||
struct default_value const default_values[] = {
|
||||
{WIFI_SSID, "HK_BOB_24G"},
|
||||
{WIFI_SSID, ""},
|
||||
{WIFI_PASSWORD, ""},
|
||||
{NETIF_TYPE, "dhcp"},
|
||||
{NETIF_HOSTNAME, "powermate"},
|
||||
@@ -39,22 +48,29 @@ struct default_value const default_values[] = {
|
||||
{NETIF_DNS1, "8.8.8.8"},
|
||||
{NETIF_DNS2, "8.8.4.4"},
|
||||
{WIFI_MODE, "apsta"},
|
||||
{AP_SSID, "odroid-pm"},
|
||||
{AP_PASSWORD, "powermate"},
|
||||
{AP_SSID, "powermate"},
|
||||
{AP_PASSWORD, "hardkernel"},
|
||||
{VIN_CURRENT_LIMIT, "4.0"},
|
||||
{MAIN_CURRENT_LIMIT, "3.0"},
|
||||
{USB_CURRENT_LIMIT, "3.0"},
|
||||
{PAGE_USERNAME, "admin"},
|
||||
{PAGE_PASSWORD, "password"},
|
||||
{SENSOR_PERIOD_MS, "1000"},
|
||||
};
|
||||
|
||||
esp_err_t init_nconfig()
|
||||
{
|
||||
esp_err_t ret = nvs_open(NCONFIG_NVS_NAMESPACE, NVS_READWRITE, &handle);
|
||||
if (ret != ESP_OK) return ret;
|
||||
if (ret != ESP_OK)
|
||||
return ret;
|
||||
|
||||
for (int i = 0; i < sizeof(default_values) / sizeof(default_values[0]); ++i) {
|
||||
for (int i = 0; i < sizeof(default_values) / sizeof(default_values[0]); ++i)
|
||||
{
|
||||
// check key is not exist or value is null
|
||||
size_t len = 0;
|
||||
nconfig_get_str_len(default_values[i].type, &len);
|
||||
if (len <= 1) // nconfig_get_str_len return err or value is '\0'
|
||||
if (nconfig_value_is_not_set(default_values[i].type))
|
||||
{
|
||||
if (nconfig_write(default_values[i].type, default_values[i].value) != ESP_OK) // if nconfig write fail, system panic
|
||||
if (nconfig_write(default_values[i].type, default_values[i].value) != ESP_OK)
|
||||
// if nconfig write fail, system panic
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
@@ -62,17 +78,25 @@ esp_err_t init_nconfig()
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nconfig_write(enum nconfig_type type, const char* data)
|
||||
void reset_nconfig()
|
||||
{
|
||||
return nvs_set_str(handle, keys[type], data);
|
||||
nvs_erase_all(handle);
|
||||
led_set(LED_RED, BLINK_FAST);
|
||||
start_reboot_timer(1);
|
||||
}
|
||||
|
||||
esp_err_t nconfig_delete(enum nconfig_type type)
|
||||
bool nconfig_value_is_not_set(enum nconfig_type type)
|
||||
{
|
||||
return nvs_erase_key(handle, keys[type]);
|
||||
size_t len = 0;
|
||||
esp_err_t err = nconfig_get_str_len(type, &len);
|
||||
return (err != ESP_OK || len <= 1);
|
||||
}
|
||||
|
||||
esp_err_t nconfig_get_str_len(enum nconfig_type type, size_t *len)
|
||||
esp_err_t nconfig_write(enum nconfig_type type, const char* data) { return nvs_set_str(handle, keys[type], data); }
|
||||
|
||||
esp_err_t nconfig_delete(enum nconfig_type type) { return nvs_erase_key(handle, keys[type]); }
|
||||
|
||||
esp_err_t nconfig_get_str_len(enum nconfig_type type, size_t* len)
|
||||
{
|
||||
return nvs_get_str(handle, keys[type], NULL, len);
|
||||
}
|
||||
@@ -85,4 +109,4 @@ esp_err_t nconfig_read(enum nconfig_type type, char* data, size_t len)
|
||||
esp_err_t nconfig_read_bool(enum nconfig_type type, char* data, size_t len)
|
||||
{
|
||||
return nvs_get_str(handle, keys[type], data, &len);
|
||||
}
|
||||
}
|
||||
|
||||
1
main/proto/.clang-format
Normal file
1
main/proto/.clang-format
Normal file
@@ -0,0 +1 @@
|
||||
DisableFormat: true
|
||||
27
main/proto/status.pb.c
Normal file
27
main/proto/status.pb.c
Normal file
@@ -0,0 +1,27 @@
|
||||
/* Automatically generated nanopb constant definitions */
|
||||
/* Generated by nanopb-0.4.8 */
|
||||
|
||||
#include "status.pb.h"
|
||||
#if PB_PROTO_HEADER_VERSION != 40
|
||||
#error Regenerate this file with the current version of nanopb generator.
|
||||
#endif
|
||||
|
||||
PB_BIND(SensorChannelData, SensorChannelData, AUTO)
|
||||
|
||||
|
||||
PB_BIND(SensorData, SensorData, AUTO)
|
||||
|
||||
|
||||
PB_BIND(WifiStatus, WifiStatus, AUTO)
|
||||
|
||||
|
||||
PB_BIND(UartData, UartData, AUTO)
|
||||
|
||||
|
||||
PB_BIND(LoadSwStatus, LoadSwStatus, AUTO)
|
||||
|
||||
|
||||
PB_BIND(StatusMessage, StatusMessage, AUTO)
|
||||
|
||||
|
||||
|
||||
181
main/proto/status.pb.h
Normal file
181
main/proto/status.pb.h
Normal file
@@ -0,0 +1,181 @@
|
||||
/* Automatically generated nanopb header */
|
||||
/* Generated by nanopb-0.4.8 */
|
||||
|
||||
#ifndef PB_STATUS_PB_H_INCLUDED
|
||||
#define PB_STATUS_PB_H_INCLUDED
|
||||
#include <pb.h>
|
||||
|
||||
#if PB_PROTO_HEADER_VERSION != 40
|
||||
#error Regenerate this file with the current version of nanopb generator.
|
||||
#endif
|
||||
|
||||
/* Struct definitions */
|
||||
/* Represents data for a single sensor channel */
|
||||
typedef struct _SensorChannelData {
|
||||
float voltage;
|
||||
float current;
|
||||
float power;
|
||||
} SensorChannelData;
|
||||
|
||||
/* Contains data for all sensor channels and system info */
|
||||
typedef struct _SensorData {
|
||||
bool has_usb;
|
||||
SensorChannelData usb;
|
||||
bool has_main;
|
||||
SensorChannelData main;
|
||||
bool has_vin;
|
||||
SensorChannelData vin;
|
||||
uint64_t timestamp_ms;
|
||||
uint64_t uptime_ms;
|
||||
} SensorData;
|
||||
|
||||
/* Contains WiFi connection status */
|
||||
typedef struct _WifiStatus {
|
||||
bool connected;
|
||||
pb_callback_t ssid;
|
||||
int32_t rssi;
|
||||
pb_callback_t ip_address;
|
||||
} WifiStatus;
|
||||
|
||||
/* Contains raw UART data */
|
||||
typedef struct _UartData {
|
||||
pb_callback_t data;
|
||||
} UartData;
|
||||
|
||||
/* Contains load sw status */
|
||||
typedef struct _LoadSwStatus {
|
||||
bool main;
|
||||
bool usb;
|
||||
} LoadSwStatus;
|
||||
|
||||
/* Top-level message for all websocket communication */
|
||||
typedef struct _StatusMessage {
|
||||
pb_size_t which_payload;
|
||||
union {
|
||||
SensorData sensor_data;
|
||||
WifiStatus wifi_status;
|
||||
LoadSwStatus sw_status;
|
||||
UartData uart_data;
|
||||
} payload;
|
||||
} StatusMessage;
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define SensorChannelData_init_default {0, 0, 0}
|
||||
#define SensorData_init_default {false, SensorChannelData_init_default, false, SensorChannelData_init_default, false, SensorChannelData_init_default, 0, 0}
|
||||
#define WifiStatus_init_default {0, {{NULL}, NULL}, 0, {{NULL}, NULL}}
|
||||
#define UartData_init_default {{{NULL}, NULL}}
|
||||
#define LoadSwStatus_init_default {0, 0}
|
||||
#define StatusMessage_init_default {0, {SensorData_init_default}}
|
||||
#define SensorChannelData_init_zero {0, 0, 0}
|
||||
#define SensorData_init_zero {false, SensorChannelData_init_zero, false, SensorChannelData_init_zero, false, SensorChannelData_init_zero, 0, 0}
|
||||
#define WifiStatus_init_zero {0, {{NULL}, NULL}, 0, {{NULL}, NULL}}
|
||||
#define UartData_init_zero {{{NULL}, NULL}}
|
||||
#define LoadSwStatus_init_zero {0, 0}
|
||||
#define StatusMessage_init_zero {0, {SensorData_init_zero}}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
#define SensorChannelData_voltage_tag 1
|
||||
#define SensorChannelData_current_tag 2
|
||||
#define SensorChannelData_power_tag 3
|
||||
#define SensorData_usb_tag 1
|
||||
#define SensorData_main_tag 2
|
||||
#define SensorData_vin_tag 3
|
||||
#define SensorData_timestamp_ms_tag 4
|
||||
#define SensorData_uptime_ms_tag 5
|
||||
#define WifiStatus_connected_tag 1
|
||||
#define WifiStatus_ssid_tag 2
|
||||
#define WifiStatus_rssi_tag 3
|
||||
#define WifiStatus_ip_address_tag 4
|
||||
#define UartData_data_tag 1
|
||||
#define LoadSwStatus_main_tag 1
|
||||
#define LoadSwStatus_usb_tag 2
|
||||
#define StatusMessage_sensor_data_tag 1
|
||||
#define StatusMessage_wifi_status_tag 2
|
||||
#define StatusMessage_sw_status_tag 3
|
||||
#define StatusMessage_uart_data_tag 4
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
#define SensorChannelData_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, voltage, 1) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, current, 2) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, power, 3)
|
||||
#define SensorChannelData_CALLBACK NULL
|
||||
#define SensorChannelData_DEFAULT NULL
|
||||
|
||||
#define SensorData_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, usb, 1) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, main, 2) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, vin, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT64, timestamp_ms, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT64, uptime_ms, 5)
|
||||
#define SensorData_CALLBACK NULL
|
||||
#define SensorData_DEFAULT NULL
|
||||
#define SensorData_usb_MSGTYPE SensorChannelData
|
||||
#define SensorData_main_MSGTYPE SensorChannelData
|
||||
#define SensorData_vin_MSGTYPE SensorChannelData
|
||||
|
||||
#define WifiStatus_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, BOOL, connected, 1) \
|
||||
X(a, CALLBACK, SINGULAR, STRING, ssid, 2) \
|
||||
X(a, STATIC, SINGULAR, INT32, rssi, 3) \
|
||||
X(a, CALLBACK, SINGULAR, STRING, ip_address, 4)
|
||||
#define WifiStatus_CALLBACK pb_default_field_callback
|
||||
#define WifiStatus_DEFAULT NULL
|
||||
|
||||
#define UartData_FIELDLIST(X, a) \
|
||||
X(a, CALLBACK, SINGULAR, BYTES, data, 1)
|
||||
#define UartData_CALLBACK pb_default_field_callback
|
||||
#define UartData_DEFAULT NULL
|
||||
|
||||
#define LoadSwStatus_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, BOOL, main, 1) \
|
||||
X(a, STATIC, SINGULAR, BOOL, usb, 2)
|
||||
#define LoadSwStatus_CALLBACK NULL
|
||||
#define LoadSwStatus_DEFAULT NULL
|
||||
|
||||
#define StatusMessage_FIELDLIST(X, a) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,sensor_data,payload.sensor_data), 1) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,wifi_status,payload.wifi_status), 2) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,sw_status,payload.sw_status), 3) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,uart_data,payload.uart_data), 4)
|
||||
#define StatusMessage_CALLBACK NULL
|
||||
#define StatusMessage_DEFAULT NULL
|
||||
#define StatusMessage_payload_sensor_data_MSGTYPE SensorData
|
||||
#define StatusMessage_payload_wifi_status_MSGTYPE WifiStatus
|
||||
#define StatusMessage_payload_sw_status_MSGTYPE LoadSwStatus
|
||||
#define StatusMessage_payload_uart_data_MSGTYPE UartData
|
||||
|
||||
extern const pb_msgdesc_t SensorChannelData_msg;
|
||||
extern const pb_msgdesc_t SensorData_msg;
|
||||
extern const pb_msgdesc_t WifiStatus_msg;
|
||||
extern const pb_msgdesc_t UartData_msg;
|
||||
extern const pb_msgdesc_t LoadSwStatus_msg;
|
||||
extern const pb_msgdesc_t StatusMessage_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define SensorChannelData_fields &SensorChannelData_msg
|
||||
#define SensorData_fields &SensorData_msg
|
||||
#define WifiStatus_fields &WifiStatus_msg
|
||||
#define UartData_fields &UartData_msg
|
||||
#define LoadSwStatus_fields &LoadSwStatus_msg
|
||||
#define StatusMessage_fields &StatusMessage_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
/* WifiStatus_size depends on runtime parameters */
|
||||
/* UartData_size depends on runtime parameters */
|
||||
/* StatusMessage_size depends on runtime parameters */
|
||||
#define LoadSwStatus_size 4
|
||||
#define STATUS_PB_H_MAX_SIZE SensorData_size
|
||||
#define SensorChannelData_size 15
|
||||
#define SensorData_size 73
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif
|
||||
222
main/service/auth.c
Normal file
222
main/service/auth.c
Normal file
@@ -0,0 +1,222 @@
|
||||
#include "auth.h"
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_random.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
static const char* TAG = "AUTH";
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char token[TOKEN_LENGTH];
|
||||
bool active;
|
||||
time_t creation_time;
|
||||
} auth_token_t;
|
||||
|
||||
static auth_token_t s_tokens[MAX_TOKENS];
|
||||
static SemaphoreHandle_t s_token_mutex;
|
||||
|
||||
void auth_init(void)
|
||||
{
|
||||
s_token_mutex = xSemaphoreCreateMutex();
|
||||
if (s_token_mutex == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to create token mutex");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < MAX_TOKENS; i++)
|
||||
{
|
||||
s_tokens[i].active = false;
|
||||
s_tokens[i].token[0] = '\0';
|
||||
}
|
||||
ESP_LOGI(TAG, "Auth module initialized.");
|
||||
}
|
||||
|
||||
char* auth_generate_token(void)
|
||||
{
|
||||
if (xSemaphoreTake(s_token_mutex, portMAX_DELAY) != pdTRUE)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to take token mutex");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int free_slot = -1;
|
||||
for (int i = 0; i < MAX_TOKENS; i++)
|
||||
{
|
||||
if (!s_tokens[i].active)
|
||||
{
|
||||
free_slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (free_slot == -1)
|
||||
{
|
||||
ESP_LOGW(TAG, "No free token slots available. Invalidating oldest token.");
|
||||
time_t oldest_time = time(NULL);
|
||||
int oldest_idx = -1;
|
||||
for (int i = 0; i < MAX_TOKENS; i++)
|
||||
{
|
||||
if (s_tokens[i].active && s_tokens[i].creation_time < oldest_time)
|
||||
{
|
||||
oldest_time = s_tokens[i].creation_time;
|
||||
oldest_idx = i;
|
||||
}
|
||||
}
|
||||
if (oldest_idx != -1)
|
||||
{
|
||||
s_tokens[oldest_idx].active = false;
|
||||
free_slot = oldest_idx;
|
||||
ESP_LOGI(TAG, "Oldest token at index %d invalidated.", oldest_idx);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Could not find an oldest token to invalidate. This should not happen if all are active.");
|
||||
xSemaphoreGive(s_token_mutex);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
char* new_token = (char*)malloc(TOKEN_LENGTH);
|
||||
if (new_token == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for new token");
|
||||
xSemaphoreGive(s_token_mutex);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < TOKEN_LENGTH - 1; i++)
|
||||
{
|
||||
new_token[i] = charset[esp_random() % (sizeof(charset) - 1)];
|
||||
}
|
||||
new_token[TOKEN_LENGTH - 1] = '\0';
|
||||
|
||||
strncpy(s_tokens[free_slot].token, new_token, TOKEN_LENGTH);
|
||||
s_tokens[free_slot].active = true;
|
||||
s_tokens[free_slot].creation_time = time(NULL);
|
||||
|
||||
ESP_LOGI(TAG, "Generated new token at slot %d: %s", free_slot, new_token);
|
||||
|
||||
xSemaphoreGive(s_token_mutex);
|
||||
return new_token;
|
||||
}
|
||||
|
||||
bool auth_validate_token(const char* token)
|
||||
{
|
||||
if (token == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (xSemaphoreTake(s_token_mutex, portMAX_DELAY) != pdTRUE)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to take token mutex");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool valid = false;
|
||||
for (int i = 0; i < MAX_TOKENS; i++)
|
||||
{
|
||||
if (s_tokens[i].active && strcmp(s_tokens[i].token, token) == 0)
|
||||
{
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_token_mutex);
|
||||
return valid;
|
||||
}
|
||||
|
||||
void auth_invalidate_token(const char* token)
|
||||
{
|
||||
if (token == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (xSemaphoreTake(s_token_mutex, portMAX_DELAY) != pdTRUE)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to take token mutex");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < MAX_TOKENS; i++)
|
||||
{
|
||||
if (s_tokens[i].active && strcmp(s_tokens[i].token, token) == 0)
|
||||
{
|
||||
s_tokens[i].active = false;
|
||||
s_tokens[i].token[0] = '\0'; // Clear token string
|
||||
ESP_LOGI(TAG, "Token at slot %d invalidated.", i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_token_mutex);
|
||||
}
|
||||
|
||||
void auth_cleanup_expired_tokens(void) { ESP_LOGD(TAG, "auth_cleanup_expired_tokens called (no-op for now)."); }
|
||||
|
||||
static const char* get_token_from_header(httpd_req_t* req)
|
||||
{
|
||||
char* auth_header = NULL;
|
||||
size_t buf_len;
|
||||
|
||||
if (httpd_req_get_hdr_value_len(req, "Authorization") == 0)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buf_len = httpd_req_get_hdr_value_len(req, "Authorization") + 1;
|
||||
auth_header = (char*)malloc(buf_len);
|
||||
if (auth_header == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for auth header");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (httpd_req_get_hdr_value_str(req, "Authorization", auth_header, buf_len) != ESP_OK)
|
||||
{
|
||||
free(auth_header);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (strncmp(auth_header, "Bearer ", 7) == 0)
|
||||
{
|
||||
const char* token = auth_header + 7;
|
||||
return token;
|
||||
}
|
||||
|
||||
free(auth_header);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
esp_err_t api_auth_check(httpd_req_t* req)
|
||||
{
|
||||
const char* token = get_token_from_header(req);
|
||||
|
||||
if (token == NULL)
|
||||
{
|
||||
ESP_LOGW(TAG, "API access attempt without token for URI: %s", req->uri);
|
||||
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Authorization token required");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (!auth_validate_token(token))
|
||||
{
|
||||
ESP_LOGW(TAG, "API access attempt with invalid token for URI: %s", req->uri);
|
||||
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Invalid or expired token");
|
||||
free((void*)token - 7);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Token validated for URI: %s", req->uri);
|
||||
free((void*)token - 7);
|
||||
return ESP_OK;
|
||||
}
|
||||
28
main/service/auth.h
Normal file
28
main/service/auth.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef AUTH_H
|
||||
#define AUTH_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
#define MAX_TOKENS 4
|
||||
#define TOKEN_LENGTH 33 // 32 characters + null terminator
|
||||
|
||||
// Function to initialize the authentication module
|
||||
void auth_init(void);
|
||||
|
||||
// Function to generate a new token
|
||||
char* auth_generate_token(void);
|
||||
|
||||
// Function to validate a token
|
||||
bool auth_validate_token(const char* token);
|
||||
|
||||
// Function to invalidate a token (e.g., on logout)
|
||||
void auth_invalidate_token(const char* token);
|
||||
|
||||
// Function to clean up expired tokens (if any)
|
||||
void auth_cleanup_expired_tokens(void);
|
||||
|
||||
esp_err_t api_auth_check(httpd_req_t* req);
|
||||
|
||||
#endif // AUTH_H
|
||||
21
main/service/climit.h
Normal file
21
main/service/climit.h
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Created by shinys on 25. 9. 4..
|
||||
//
|
||||
|
||||
#ifndef ODROID_POWER_MATE_CLIMIT_H
|
||||
#define ODROID_POWER_MATE_CLIMIT_H
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define VIN_CURRENT_LIMIT_MAX 8.0f
|
||||
#define MAIN_CURRENT_LIMIT_MAX 7.5f
|
||||
#define USB_CURRENT_LIMIT_MAX 4.5f
|
||||
|
||||
esp_err_t climit_set_vin(double value);
|
||||
esp_err_t climit_set_main(double value);
|
||||
esp_err_t climit_set_usb(double value);
|
||||
bool is_overcurrent();
|
||||
|
||||
#endif // ODROID_POWER_MATE_CLIMIT_H
|
||||
@@ -1,49 +1,26 @@
|
||||
#include "webserver.h"
|
||||
#include "auth.h"
|
||||
#include "cJSON.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_timer.h"
|
||||
#include "sw.h"
|
||||
#include "webserver.h"
|
||||
|
||||
#define GPIO_12V_SWITCH CONFIG_GPIO_SW_12V
|
||||
#define GPIO_5V_SWITCH CONFIG_GPIO_SW_5V
|
||||
#define GPIO_POWER_TRIGGER CONFIG_GPIO_TRIGGER_POWER
|
||||
#define GPIO_RESET_TRIGGER CONFIG_GPIO_TRIGGER_RESET
|
||||
|
||||
static bool status_12v_on = false;
|
||||
static bool status_5v_on = false;
|
||||
static SemaphoreHandle_t state_mutex;
|
||||
static esp_timer_handle_t power_trigger_timer;
|
||||
static esp_timer_handle_t reset_trigger_timer;
|
||||
|
||||
/**
|
||||
* @brief 타이머 만료 시 GPIO를 다시 HIGH로 설정하는 콜백 함수
|
||||
*/
|
||||
static void trigger_off_callback(void* arg)
|
||||
static esp_err_t control_get_handler(httpd_req_t* req)
|
||||
{
|
||||
gpio_num_t gpio_pin = (int) arg;
|
||||
gpio_set_level(gpio_pin, 1); // 핀을 다시 HIGH로 복구
|
||||
}
|
||||
esp_err_t err = api_auth_check(req);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
static void update_gpio_switches()
|
||||
{
|
||||
gpio_set_level(GPIO_12V_SWITCH, status_12v_on);
|
||||
gpio_set_level(GPIO_5V_SWITCH, status_5v_on);
|
||||
}
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
|
||||
static esp_err_t control_get_handler(httpd_req_t *req)
|
||||
{
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "load_12v_on", get_main_load_switch());
|
||||
cJSON_AddBoolToObject(root, "load_5v_on", get_usb_load_switch());
|
||||
|
||||
xSemaphoreTake(state_mutex, portMAX_DELAY);
|
||||
cJSON_AddBoolToObject(root, "load_12v_on", status_12v_on);
|
||||
cJSON_AddBoolToObject(root, "load_5v_on", status_5v_on);
|
||||
xSemaphoreGive(state_mutex);
|
||||
|
||||
char *json_string = cJSON_Print(root);
|
||||
char* json_string = cJSON_Print(root);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, json_string, strlen(json_string));
|
||||
|
||||
@@ -53,64 +30,56 @@ static esp_err_t control_get_handler(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t control_post_handler(httpd_req_t *req)
|
||||
static esp_err_t control_post_handler(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = api_auth_check(req);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
char buf[128];
|
||||
int ret, remaining = req->content_len;
|
||||
|
||||
if (remaining >= sizeof(buf)) {
|
||||
if (remaining >= sizeof(buf))
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Request content too long");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ret = httpd_req_recv(req, buf, remaining);
|
||||
if (ret <= 0) {
|
||||
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
if (ret <= 0)
|
||||
{
|
||||
if (ret == HTTPD_SOCK_ERR_TIMEOUT)
|
||||
{
|
||||
httpd_resp_send_408(req);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
if (root == NULL) {
|
||||
cJSON* root = cJSON_Parse(buf);
|
||||
if (root == NULL)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON format");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
bool state_changed = false;
|
||||
xSemaphoreTake(state_mutex, portMAX_DELAY);
|
||||
cJSON* item_12v = cJSON_GetObjectItem(root, "load_12v_on");
|
||||
if (cJSON_IsBool(item_12v))
|
||||
set_main_load_switch(cJSON_IsTrue(item_12v));
|
||||
|
||||
cJSON *item_12v = cJSON_GetObjectItem(root, "load_12v_on");
|
||||
if (cJSON_IsBool(item_12v)) {
|
||||
status_12v_on = cJSON_IsTrue(item_12v);
|
||||
state_changed = true;
|
||||
}
|
||||
cJSON* item_5v = cJSON_GetObjectItem(root, "load_5v_on");
|
||||
if (cJSON_IsBool(item_5v))
|
||||
set_usb_load_switch(cJSON_IsTrue(item_5v));
|
||||
|
||||
cJSON *item_5v = cJSON_GetObjectItem(root, "load_5v_on");
|
||||
if (cJSON_IsBool(item_5v)) {
|
||||
status_5v_on = cJSON_IsTrue(item_5v);
|
||||
state_changed = true;
|
||||
}
|
||||
cJSON* power_trigger = cJSON_GetObjectItem(root, "power_trigger");
|
||||
if (cJSON_IsTrue(power_trigger))
|
||||
trig_power();
|
||||
|
||||
if (state_changed) {
|
||||
update_gpio_switches();
|
||||
}
|
||||
xSemaphoreGive(state_mutex);
|
||||
|
||||
cJSON *power_trigger = cJSON_GetObjectItem(root, "power_trigger");
|
||||
if (cJSON_IsTrue(power_trigger)) {
|
||||
gpio_set_level(GPIO_POWER_TRIGGER, 0);
|
||||
esp_timer_stop(power_trigger_timer); // Stop timer if it's already running
|
||||
ESP_ERROR_CHECK(esp_timer_start_once(power_trigger_timer, 3000000)); // 3초
|
||||
}
|
||||
|
||||
cJSON *reset_trigger = cJSON_GetObjectItem(root, "reset_trigger");
|
||||
if (cJSON_IsTrue(reset_trigger)) {
|
||||
gpio_set_level(GPIO_RESET_TRIGGER, 0);
|
||||
esp_timer_stop(reset_trigger_timer); // Stop timer if it's already running
|
||||
ESP_ERROR_CHECK(esp_timer_start_once(reset_trigger_timer, 3000000)); // 3초
|
||||
}
|
||||
cJSON* reset_trigger = cJSON_GetObjectItem(root, "reset_trigger");
|
||||
if (cJSON_IsTrue(reset_trigger))
|
||||
trig_reset();
|
||||
|
||||
cJSON_Delete(root);
|
||||
|
||||
@@ -118,64 +87,13 @@ static esp_err_t control_post_handler(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void control_module_init(void)
|
||||
{
|
||||
state_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
gpio_config_t switch_conf = {
|
||||
.pin_bit_mask = (1ULL << GPIO_12V_SWITCH) | (1ULL << GPIO_5V_SWITCH),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&switch_conf);
|
||||
update_gpio_switches();
|
||||
|
||||
gpio_config_t trigger_conf = {
|
||||
.pin_bit_mask = (1ULL << GPIO_POWER_TRIGGER) | (1ULL << GPIO_RESET_TRIGGER),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&trigger_conf);
|
||||
gpio_set_level(GPIO_POWER_TRIGGER, 1);
|
||||
gpio_set_level(GPIO_RESET_TRIGGER, 1);
|
||||
|
||||
const esp_timer_create_args_t power_timer_args = {
|
||||
.callback = &trigger_off_callback,
|
||||
.arg = (void*) GPIO_POWER_TRIGGER,
|
||||
.name = "power_trigger_off"
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&power_timer_args, &power_trigger_timer));
|
||||
|
||||
const esp_timer_create_args_t reset_timer_args = {
|
||||
.callback = &trigger_off_callback,
|
||||
.arg = (void*) GPIO_RESET_TRIGGER,
|
||||
.name = "reset_trigger_off"
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&reset_timer_args, &reset_trigger_timer));
|
||||
|
||||
}
|
||||
|
||||
void register_control_endpoint(httpd_handle_t server)
|
||||
{
|
||||
control_module_init();
|
||||
httpd_uri_t get_uri = {
|
||||
.uri = "/api/control",
|
||||
.method = HTTP_GET,
|
||||
.handler = control_get_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
init_sw();
|
||||
httpd_uri_t get_uri = {.uri = "/api/control", .method = HTTP_GET, .handler = control_get_handler, .user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &get_uri);
|
||||
|
||||
httpd_uri_t post_uri = {
|
||||
.uri = "/api/control",
|
||||
.method = HTTP_POST,
|
||||
.handler = control_post_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
.uri = "/api/control", .method = HTTP_POST, .handler = control_post_handler, .user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &post_uri);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
#include "datalog.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include "esp_littlefs.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char* TAG = "DATALOG";
|
||||
static const char* LOG_FILE_PATH = "/littlefs/datalog.csv";
|
||||
#define MAX_LOG_SIZE (1024 * 1024)
|
||||
|
||||
void datalog_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing DataLog with LittleFS");
|
||||
|
||||
esp_vfs_littlefs_conf_t conf = {
|
||||
.base_path = "/littlefs",
|
||||
.partition_label = "littlefs",
|
||||
.format_if_mount_failed = true,
|
||||
.dont_mount = false,
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_vfs_littlefs_register(&conf);
|
||||
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
if (ret == ESP_FAIL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to mount or format filesystem");
|
||||
}
|
||||
else if (ret == ESP_ERR_NOT_FOUND)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to find LittleFS partition");
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to initialize LittleFS (%s)", esp_err_to_name(ret));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
size_t total = 0, used = 0;
|
||||
ret = esp_littlefs_info(NULL, &total, &used);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to get LittleFS partition information (%s)", esp_err_to_name(ret));
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
FILE* f = fopen(LOG_FILE_PATH, "r");
|
||||
if (f == NULL)
|
||||
{
|
||||
ESP_LOGI(TAG, "Log file not found, creating new one.");
|
||||
FILE* f_write = fopen(LOG_FILE_PATH, "w");
|
||||
if (f_write == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to create log file.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add header
|
||||
fprintf(f_write, "timestamp,voltage,current,power\n");
|
||||
fclose(f_write);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGI(TAG, "Log file found.");
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
void datalog_add(uint32_t timestamp, float voltage, float current, float power)
|
||||
{
|
||||
char new_line[100];
|
||||
int new_line_len = snprintf(new_line, sizeof(new_line), "%lu,%.3f,%.3f,%.3f\n", timestamp, voltage, current, power);
|
||||
|
||||
struct stat st;
|
||||
long size = 0;
|
||||
if (stat(LOG_FILE_PATH, &st) == 0)
|
||||
{
|
||||
size = st.st_size;
|
||||
}
|
||||
|
||||
if (size + new_line_len <= MAX_LOG_SIZE)
|
||||
{
|
||||
FILE* f = fopen(LOG_FILE_PATH, "a");
|
||||
if (f == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to open log file for appending.");
|
||||
return;
|
||||
}
|
||||
fputs(new_line, f);
|
||||
fclose(f);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGI(TAG, "Log file is full. Rotating log file.");
|
||||
FILE* f = fopen(LOG_FILE_PATH, "r+");
|
||||
if (f == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to open log file for rotation.");
|
||||
return;
|
||||
}
|
||||
|
||||
long size_to_remove = (size + new_line_len) - MAX_LOG_SIZE;
|
||||
char line[256];
|
||||
|
||||
// Keep header
|
||||
if (fgets(line, sizeof(line), f) == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Could not read header");
|
||||
fclose(f);
|
||||
return;
|
||||
}
|
||||
long header_len = strlen(line);
|
||||
|
||||
// Find the starting position of the data to keep (read position)
|
||||
fseek(f, header_len, SEEK_SET);
|
||||
long bytes_skipped = 0;
|
||||
while (fgets(line, sizeof(line), f) != NULL)
|
||||
{
|
||||
bytes_skipped += strlen(line);
|
||||
if (bytes_skipped >= size_to_remove)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
long read_pos = ftell(f) - strlen(line);
|
||||
long write_pos = header_len;
|
||||
|
||||
char buffer[256];
|
||||
|
||||
while (1)
|
||||
{
|
||||
fseek(f, read_pos, SEEK_SET);
|
||||
size_t bytes_read = fread(buffer, 1, sizeof(buffer), f);
|
||||
if (bytes_read == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
read_pos = ftell(f);
|
||||
|
||||
fseek(f, write_pos, SEEK_SET);
|
||||
fwrite(buffer, 1, bytes_read, f);
|
||||
write_pos = ftell(f);
|
||||
}
|
||||
|
||||
if (ftruncate(fileno(f), write_pos) != 0) {
|
||||
ESP_LOGE(TAG, "Failed to truncate log file.");
|
||||
}
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
fputs(new_line, f);
|
||||
fclose(f);
|
||||
|
||||
ESP_LOGI(TAG, "Log file rotated successfully.");
|
||||
}
|
||||
}
|
||||
|
||||
const char* datalog_get_path(void)
|
||||
{
|
||||
return LOG_FILE_PATH;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
#ifndef MAIN_SERVICE_DATALOG_H_
|
||||
#define MAIN_SERVICE_DATALOG_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void datalog_init(void);
|
||||
void datalog_add(uint32_t timestamp, float voltage, float current, float power);
|
||||
const char* datalog_get_path(void);
|
||||
|
||||
#endif /* MAIN_SERVICE_DATALOG_H_ */
|
||||
182
main/service/dbg_console.c
Normal file
182
main/service/dbg_console.c
Normal file
@@ -0,0 +1,182 @@
|
||||
#include "dbg_console.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "argtable3/argtable3.h"
|
||||
#include "esp_console.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "wifi.h"
|
||||
|
||||
|
||||
/* 'wifi_scan' command */
|
||||
static int wifi_scan_handler(int argc, char** argv)
|
||||
{
|
||||
printf("Scanning for Wi-Fi networks...\n");
|
||||
|
||||
wifi_ap_record_t* ap_records;
|
||||
uint16_t count = 0;
|
||||
|
||||
wifi_scan_aps(&ap_records, &count);
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
printf("No APs found.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("Found %d APs:\n", count);
|
||||
printf(" %-32s %-4s %s\n", "SSID", "RSSI", "Auth Mode");
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
printf(" %-32s %-4d %s\n", ap_records[i].ssid, ap_records[i].rssi, auth_mode_str(ap_records[i].authmode));
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
free(ap_records);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void register_wifi_scan(void)
|
||||
{
|
||||
const esp_console_cmd_t cmd = {
|
||||
.command = "wifi_scan",
|
||||
.help = "Scan for available Wi-Fi networks",
|
||||
.hint = NULL,
|
||||
.func = &wifi_scan_handler,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
|
||||
}
|
||||
|
||||
static struct
|
||||
{
|
||||
struct arg_str* ssid;
|
||||
struct arg_str* password;
|
||||
struct arg_end* end;
|
||||
} wifi_connect_args;
|
||||
|
||||
static int wifi_connect_handler(int argc, char** argv)
|
||||
{
|
||||
int nerrors = arg_parse(argc, argv, (void**)&wifi_connect_args);
|
||||
if (nerrors != 0)
|
||||
{
|
||||
arg_print_errors(stderr, wifi_connect_args.end, argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* ssid = wifi_connect_args.ssid->sval[0];
|
||||
char password[64] = "";
|
||||
|
||||
if (wifi_connect_args.password->count != 0)
|
||||
strncpy(password, wifi_connect_args.password->sval[0], sizeof(password));
|
||||
|
||||
printf("Attempting to connect to SSID: %s\n", ssid);
|
||||
|
||||
esp_err_t err = wifi_sta_set_ap(ssid, password);
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
printf("Wi-Fi credentials set. The device will attempt to connect.\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("Failed to set Wi-Fi credentials.\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void register_wifi_connect(void)
|
||||
{
|
||||
wifi_connect_args.ssid = arg_str1(NULL, NULL, "<ssid>", "SSID of the network to connect to");
|
||||
wifi_connect_args.password = arg_str0(NULL, NULL, "<password>", "Password of the network");
|
||||
wifi_connect_args.end = arg_end(2);
|
||||
|
||||
const esp_console_cmd_t cmd = {.command = "wifi_connect",
|
||||
.help = "Connect to a Wi-Fi network",
|
||||
.hint = NULL,
|
||||
.func = &wifi_connect_handler,
|
||||
.argtable = &wifi_connect_args};
|
||||
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
|
||||
}
|
||||
|
||||
/* 'wifi_status' command */
|
||||
static int wifi_status_handler(int argc, char** argv)
|
||||
{
|
||||
wifi_ap_record_t ap_info;
|
||||
esp_err_t err = wifi_get_current_ap_info(&ap_info);
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
printf("Connected to AP:\n");
|
||||
printf(" SSID: %s\n", (char*)ap_info.ssid);
|
||||
printf(" RSSI: %d\n", ap_info.rssi);
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
err = wifi_get_current_ip_info(&ip_info);
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
char ip_str[16];
|
||||
esp_ip4addr_ntoa(&ip_info.ip, ip_str, sizeof(ip_str));
|
||||
printf(" IP Address: %s\n", ip_str);
|
||||
|
||||
esp_ip4addr_ntoa(&ip_info.gw, ip_str, sizeof(ip_str));
|
||||
printf(" Gateway: %s\n", ip_str);
|
||||
|
||||
esp_ip4addr_ntoa(&ip_info.netmask, ip_str, sizeof(ip_str));
|
||||
printf(" Subnet Mask: %s\n", ip_str);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf(" Could not get IP information: %s\n", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("Not connected to any AP.\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void register_wifi_status(void)
|
||||
{
|
||||
const esp_console_cmd_t cmd = {
|
||||
.command = "wifi_status",
|
||||
.help = "Get current Wi-Fi connection status and IP information",
|
||||
.hint = NULL,
|
||||
.func = &wifi_status_handler,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
|
||||
}
|
||||
|
||||
esp_err_t initialize_dbg_console(void)
|
||||
{
|
||||
esp_console_repl_t* repl = NULL;
|
||||
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
|
||||
|
||||
repl_config.prompt = "powermate >";
|
||||
repl_config.max_cmdline_length = 512;
|
||||
|
||||
esp_console_dev_usb_serial_jtag_config_t hw_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl));
|
||||
|
||||
esp_console_register_help_command();
|
||||
register_wifi_scan();
|
||||
register_wifi_connect();
|
||||
register_wifi_status();
|
||||
|
||||
printf("Debug console initialized.\n");
|
||||
|
||||
esp_console_start_repl(repl);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
13
main/service/dbg_console.h
Normal file
13
main/service/dbg_console.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef ODROID_POWER_MATE_DBG_CONSOLE_H
|
||||
#define ODROID_POWER_MATE_DBG_CONSOLE_H
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* @brief Initialize the debug console.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code on failure.
|
||||
*/
|
||||
esp_err_t initialize_dbg_console(void);
|
||||
|
||||
#endif // ODROID_POWER_MATE_DBG_CONSOLE_H
|
||||
@@ -2,134 +2,314 @@
|
||||
// Created by shinys on 25. 8. 18..
|
||||
//
|
||||
|
||||
|
||||
#include "monitor.h"
|
||||
#include <nconfig.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "climit.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "cJSON.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_wifi_types_generic.h"
|
||||
#include "ina226.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h" // Added for FreeRTOS tasks
|
||||
#include "ina3221.h"
|
||||
#include "pb.h"
|
||||
#include "pb_encode.h"
|
||||
#include "status.pb.h"
|
||||
#include "sw.h"
|
||||
#include "webserver.h"
|
||||
#include "wifi.h"
|
||||
#include "datalog.h"
|
||||
|
||||
#define INA226_SDA CONFIG_GPIO_INA226_SDA
|
||||
#define INA226_SCL CONFIG_GPIO_INA226_SCL
|
||||
#define CHANNEL_VIN INA3221_CHANNEL_3
|
||||
#define CHANNEL_MAIN INA3221_CHANNEL_2
|
||||
#define CHANNEL_USB INA3221_CHANNEL_1
|
||||
|
||||
ina226_t ina;
|
||||
i2c_master_bus_handle_t bus_handle;
|
||||
i2c_master_dev_handle_t dev_handle;
|
||||
#define PM_SDA CONFIG_I2C_GPIO_SDA
|
||||
#define PM_SCL CONFIG_I2C_GPIO_SCL
|
||||
|
||||
// Timer callback function to read sensor data
|
||||
static void sensor_timer_callback(void *arg)
|
||||
{
|
||||
// Generate random sensor data
|
||||
float voltage = 0;
|
||||
float current = 0;
|
||||
float power = 0;
|
||||
#define PM_INT_CRITICAL CONFIG_GPIO_INA3221_INT_CRITICAL
|
||||
#define PM_EXPANDER_RST CONFIG_GPIO_EXPANDER_RESET
|
||||
|
||||
ina226_get_bus_voltage(&ina, &voltage);
|
||||
ina226_get_power(&ina, &power);
|
||||
ina226_get_current(&ina, ¤t);
|
||||
#define PB_BUFFER_SIZE 256
|
||||
|
||||
// Get system uptime
|
||||
int64_t uptime_us = esp_timer_get_time();
|
||||
uint32_t uptime_sec = (uint32_t)(uptime_us / 1000000);
|
||||
uint32_t timestamp = (uint32_t)time(NULL);
|
||||
|
||||
datalog_add(timestamp, voltage, current, power);
|
||||
|
||||
// Create JSON object with sensor data
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(root, "type", "sensor_data");
|
||||
cJSON_AddNumberToObject(root, "voltage", voltage);
|
||||
cJSON_AddNumberToObject(root, "current", current);
|
||||
cJSON_AddNumberToObject(root, "power", power);
|
||||
cJSON_AddNumberToObject(root, "timestamp", timestamp);
|
||||
cJSON_AddNumberToObject(root, "uptime_sec", uptime_sec);
|
||||
|
||||
// Push data to WebSocket clients
|
||||
push_data_to_ws(root);
|
||||
}
|
||||
|
||||
static void status_wifi_callback(void *arg)
|
||||
{
|
||||
wifi_ap_record_t ap_info;
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
|
||||
if (wifi_get_current_ap_info(&ap_info) == ESP_OK) {
|
||||
cJSON_AddStringToObject(root, "type", "wifi_status");
|
||||
cJSON_AddBoolToObject(root, "connected", true);
|
||||
cJSON_AddStringToObject(root, "ssid", (const char *)ap_info.ssid);
|
||||
cJSON_AddNumberToObject(root, "rssi", ap_info.rssi);
|
||||
} else {
|
||||
cJSON_AddBoolToObject(root, "connected", false);
|
||||
}
|
||||
|
||||
push_data_to_ws(root);
|
||||
}
|
||||
|
||||
ina226_config_t ina_config = {
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.i2c_addr = 0x40,
|
||||
.timeout_ms = 100,
|
||||
.averages = INA226_AVERAGES_16,
|
||||
.bus_conv_time = INA226_BUS_CONV_TIME_1100_US,
|
||||
.shunt_conv_time = INA226_SHUNT_CONV_TIME_1100_US,
|
||||
.mode = INA226_MODE_SHUNT_BUS_CONT,
|
||||
.r_shunt = 0.01f,
|
||||
.max_current = 8
|
||||
};
|
||||
|
||||
static void init_ina226()
|
||||
{
|
||||
i2c_master_bus_config_t bus_config = {
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.sda_io_num = (gpio_num_t) INA226_SDA,
|
||||
.scl_io_num = (gpio_num_t) INA226_SCL,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags.enable_internal_pullup = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus_handle));
|
||||
|
||||
i2c_device_config_t dev_config = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x40,
|
||||
.scl_speed_hz = 400000,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &dev_config, &dev_handle));
|
||||
|
||||
ESP_ERROR_CHECK(ina226_init(&ina, dev_handle, &ina_config));
|
||||
}
|
||||
static const char* TAG = "monitor";
|
||||
|
||||
static esp_timer_handle_t sensor_timer;
|
||||
static esp_timer_handle_t wifi_status_timer;
|
||||
static esp_timer_handle_t long_press_timer;
|
||||
// static esp_timer_handle_t shutdown_load_sw; // No longer needed
|
||||
|
||||
static TaskHandle_t shutdown_task_handle = NULL; // Global task handle
|
||||
|
||||
ina3221_t ina3221 = {
|
||||
.shunt = {10, 10, 10},
|
||||
.mask.mask_register = INA3221_DEFAULT_MASK,
|
||||
.i2c_dev = {0},
|
||||
.config =
|
||||
{
|
||||
.mode = true, // mode selection
|
||||
.esht = true, // shunt enable
|
||||
.ebus = true, // bus enable
|
||||
.ch1 = true, // channel 1 enable
|
||||
.ch2 = true, // channel 2 enable
|
||||
.ch3 = true, // channel 3 enable
|
||||
.avg = INA3221_AVG_64, // 64 samples average
|
||||
.vbus = INA3221_CT_2116, // 2ms by channel (bus)
|
||||
.vsht = INA3221_CT_2116, // 2ms by channel (shunt)
|
||||
},
|
||||
};
|
||||
|
||||
static bool encode_string(pb_ostream_t* stream, const pb_field_t* field, void* const* arg)
|
||||
{
|
||||
const char* str = (const char*)(*arg);
|
||||
if (!str)
|
||||
{
|
||||
return true; // Nothing to encode
|
||||
}
|
||||
if (!pb_encode_tag_for_field(stream, field))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return pb_encode_string(stream, (uint8_t*)str, strlen(str));
|
||||
}
|
||||
|
||||
static void send_pb_message(const pb_msgdesc_t* fields, const void* src_struct)
|
||||
{
|
||||
uint8_t buffer[PB_BUFFER_SIZE];
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
|
||||
|
||||
if (!pb_encode(&stream, fields, src_struct))
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to encode protobuf message: %s", PB_GET_ERROR(&stream));
|
||||
return;
|
||||
}
|
||||
|
||||
push_data_to_ws(buffer, stream.bytes_written);
|
||||
}
|
||||
|
||||
static void sensor_timer_callback(void* arg)
|
||||
{
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
uint64_t timestamp_ms = (uint64_t)tv.tv_sec * 1000 + (uint64_t)tv.tv_usec / 1000;
|
||||
uint64_t uptime_ms = (uint64_t)esp_timer_get_time() / 1000;
|
||||
|
||||
StatusMessage message = StatusMessage_init_zero;
|
||||
message.which_payload = StatusMessage_sensor_data_tag;
|
||||
SensorData* sensor_data = &message.payload.sensor_data;
|
||||
|
||||
sensor_data->has_usb = true;
|
||||
sensor_data->has_main = true;
|
||||
sensor_data->has_vin = true;
|
||||
|
||||
SensorChannelData* channels[] = {&sensor_data->usb, &sensor_data->main, &sensor_data->vin};
|
||||
|
||||
for (uint8_t i = 0; i < INA3221_BUS_NUMBER; i++)
|
||||
{
|
||||
float voltage, current, power;
|
||||
ina3221_get_bus_voltage(&ina3221, i, &voltage);
|
||||
ina3221_get_shunt_value(&ina3221, i, NULL, ¤t);
|
||||
|
||||
current /= 1000.0f; // mA to A
|
||||
power = voltage * current;
|
||||
|
||||
// For protobuf
|
||||
channels[i]->voltage = voltage;
|
||||
channels[i]->current = current;
|
||||
channels[i]->power = power;
|
||||
}
|
||||
|
||||
// datalog_add(timestamp, channel_data_log);
|
||||
|
||||
sensor_data->timestamp_ms = timestamp_ms;
|
||||
sensor_data->uptime_ms = uptime_ms;
|
||||
|
||||
send_pb_message(StatusMessage_fields, &message);
|
||||
}
|
||||
|
||||
static void status_wifi_callback(void* arg)
|
||||
{
|
||||
wifi_ap_record_t ap_info;
|
||||
StatusMessage message = StatusMessage_init_zero;
|
||||
message.which_payload = StatusMessage_wifi_status_tag;
|
||||
WifiStatus* wifi_status = &message.payload.wifi_status;
|
||||
char ip_str[16];
|
||||
esp_netif_ip_info_t ip_info;
|
||||
|
||||
if (wifi_get_current_ap_info(&ap_info) == ESP_OK)
|
||||
{
|
||||
wifi_status->connected = true;
|
||||
wifi_status->ssid.funcs.encode = &encode_string;
|
||||
wifi_status->ssid.arg = (void*)ap_info.ssid;
|
||||
wifi_status->rssi = ap_info.rssi;
|
||||
}
|
||||
else
|
||||
{
|
||||
wifi_status->connected = false;
|
||||
wifi_status->ssid.arg = ""; // Empty string
|
||||
wifi_status->rssi = 0;
|
||||
}
|
||||
|
||||
if (wifi_get_current_ip_info(&ip_info) == ESP_OK)
|
||||
{
|
||||
esp_ip4addr_ntoa(&ip_info.ip, ip_str, sizeof(ip_str));
|
||||
wifi_status->ip_address.funcs.encode = &encode_string;
|
||||
wifi_status->ip_address.arg = ip_str;
|
||||
}
|
||||
else
|
||||
{
|
||||
wifi_status->ip_address.arg = ""; // Empty string
|
||||
}
|
||||
|
||||
send_pb_message(StatusMessage_fields, &message);
|
||||
}
|
||||
|
||||
// Placeholder for long press action
|
||||
static void handle_critical_long_press(void)
|
||||
{
|
||||
ESP_LOGW(TAG, "Config reset triggered...");
|
||||
reset_nconfig();
|
||||
}
|
||||
|
||||
// Timer callback for long press detection
|
||||
static void long_press_timer_callback(void* arg)
|
||||
{
|
||||
if (gpio_get_level(PM_INT_CRITICAL) == 0)
|
||||
{
|
||||
handle_critical_long_press();
|
||||
}
|
||||
}
|
||||
|
||||
// New FreeRTOS task for shutdown logic
|
||||
static void shutdown_load_sw_task(void* pvParameters)
|
||||
{
|
||||
while (1)
|
||||
{
|
||||
// Wait indefinitely for a notification from the ISR
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
|
||||
ESP_LOGW(TAG, "critical interrupt triggered (via task)");
|
||||
gpio_set_level(PM_EXPANDER_RST, 0);
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
gpio_set_level(PM_EXPANDER_RST, 1);
|
||||
config_sw();
|
||||
|
||||
// Start a 5-second timer to check for long press
|
||||
esp_timer_start_once(long_press_timer, 5000000);
|
||||
}
|
||||
}
|
||||
|
||||
static void IRAM_ATTR critical_isr_handler(void* arg)
|
||||
{
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
if (gpio_get_level(PM_INT_CRITICAL) == 0) // Falling edge
|
||||
{
|
||||
if (shutdown_task_handle != NULL)
|
||||
{
|
||||
vTaskNotifyGiveFromISR(shutdown_task_handle, &xHigherPriorityTaskWoken);
|
||||
}
|
||||
}
|
||||
else // Rising edge
|
||||
{
|
||||
// Stop the timer if the button is released
|
||||
esp_timer_stop(long_press_timer);
|
||||
}
|
||||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||||
}
|
||||
|
||||
static void gpio_init()
|
||||
{
|
||||
// critical int
|
||||
gpio_set_intr_type(PM_INT_CRITICAL, GPIO_INTR_ANYEDGE);
|
||||
gpio_set_direction(PM_INT_CRITICAL, GPIO_MODE_INPUT);
|
||||
gpio_install_isr_service(0);
|
||||
gpio_isr_handler_add(PM_INT_CRITICAL, critical_isr_handler, (void*)PM_INT_CRITICAL);
|
||||
|
||||
// rst expander
|
||||
gpio_set_level(PM_EXPANDER_RST, 1);
|
||||
gpio_set_direction(PM_EXPANDER_RST, GPIO_MODE_OUTPUT);
|
||||
}
|
||||
|
||||
esp_err_t climit_set_vin(double value)
|
||||
{
|
||||
float lim = (float)(value * 1000);
|
||||
ESP_LOGI(TAG, "Setting VIN current limit to: %fmA", lim);
|
||||
if (value > 0.0f)
|
||||
return ina3221_set_critical_alert(&ina3221, CHANNEL_VIN, lim);
|
||||
return ina3221_set_critical_alert(&ina3221, CHANNEL_VIN, (15.0f * 1000.0f));
|
||||
}
|
||||
|
||||
esp_err_t climit_set_main(double value)
|
||||
{
|
||||
float lim = (float)(value * 1000);
|
||||
ESP_LOGI(TAG, "Setting MAIN current limit to: %fmA", lim);
|
||||
if (value > 0.0f)
|
||||
return ina3221_set_critical_alert(&ina3221, CHANNEL_MAIN, lim);
|
||||
return ina3221_set_critical_alert(&ina3221, CHANNEL_VIN, (15.0f * 1000.0f));
|
||||
}
|
||||
|
||||
esp_err_t climit_set_usb(double value)
|
||||
{
|
||||
float lim = (float)(value * 1000);
|
||||
ESP_LOGI(TAG, "Setting USB current limit to: %fmA", lim);
|
||||
if (value > 0.0f)
|
||||
return ina3221_set_critical_alert(&ina3221, CHANNEL_USB, lim);
|
||||
return ina3221_set_critical_alert(&ina3221, CHANNEL_VIN, (15.0f * 1000.0f));
|
||||
}
|
||||
|
||||
void init_status_monitor()
|
||||
{
|
||||
init_ina226();
|
||||
datalog_init();
|
||||
gpio_init();
|
||||
ESP_ERROR_CHECK(ina3221_init_desc(&ina3221, 0x40, 0, PM_SDA, PM_SCL));
|
||||
|
||||
// Timer configuration
|
||||
const esp_timer_create_args_t sensor_timer_args = {
|
||||
.callback = &sensor_timer_callback,
|
||||
.name = "sensor_reading_timer" // Optional name for debugging
|
||||
};
|
||||
double lim;
|
||||
char buf[10];
|
||||
|
||||
const esp_timer_create_args_t wifi_timer_args = {
|
||||
.callback = &status_wifi_callback,
|
||||
.name = "wifi_status_timer" // Optional name for debugging
|
||||
};
|
||||
nconfig_read(VIN_CURRENT_LIMIT, buf, sizeof(buf));
|
||||
lim = atof(buf);
|
||||
climit_set_vin(lim);
|
||||
|
||||
nconfig_read(MAIN_CURRENT_LIMIT, buf, sizeof(buf));
|
||||
lim = atof(buf);
|
||||
climit_set_main(lim);
|
||||
|
||||
nconfig_read(USB_CURRENT_LIMIT, buf, sizeof(buf));
|
||||
lim = atof(buf);
|
||||
climit_set_usb(lim);
|
||||
|
||||
const esp_timer_create_args_t sensor_timer_args = {.callback = &sensor_timer_callback,
|
||||
.name = "sensor_reading_timer"};
|
||||
const esp_timer_create_args_t wifi_timer_args = {.callback = &status_wifi_callback, .name = "wifi_status_timer"};
|
||||
const esp_timer_create_args_t long_press_timer_args = {.callback = &long_press_timer_callback,
|
||||
.name = "long_press_timer"};
|
||||
|
||||
ESP_ERROR_CHECK(esp_timer_create(&sensor_timer_args, &sensor_timer));
|
||||
ESP_ERROR_CHECK(esp_timer_create(&wifi_timer_args, &wifi_status_timer));
|
||||
ESP_ERROR_CHECK(esp_timer_create(&long_press_timer_args, &long_press_timer));
|
||||
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(sensor_timer, 1000000)); // 1sec
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(wifi_status_timer, 1000000 * 5)); // 5s in microseconds
|
||||
xTaskCreate(shutdown_load_sw_task, "shutdown_sw_task", configMINIMAL_STACK_SIZE * 3, NULL, 15,
|
||||
&shutdown_task_handle);
|
||||
|
||||
nconfig_read(SENSOR_PERIOD_MS, buf, sizeof(buf));
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(sensor_timer, strtol(buf, NULL, 10) * 1000));
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(wifi_status_timer, 1000000 * 5));
|
||||
}
|
||||
|
||||
esp_err_t update_sensor_period(int period)
|
||||
{
|
||||
if (period < 500 || period > 10000) // 0.5 sec ~ 10 sec
|
||||
{
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
char buf[10];
|
||||
sprintf(buf, "%d", period);
|
||||
esp_err_t err = nconfig_write(SENSOR_PERIOD_MS, buf);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_timer_stop(sensor_timer);
|
||||
return esp_timer_start_periodic(sensor_timer, period * 1000);
|
||||
}
|
||||
@@ -9,17 +9,17 @@
|
||||
|
||||
#include "esp_http_server.h"
|
||||
|
||||
// 버퍼에 저장할 데이터의 개수
|
||||
#define SENSOR_BUFFER_SIZE 100
|
||||
|
||||
// 단일 센서 데이터를 저장하기 위한 구조체
|
||||
typedef struct {
|
||||
typedef struct
|
||||
{
|
||||
float voltage;
|
||||
float current;
|
||||
float power;
|
||||
uint32_t timestamp; // 데이터를 읽은 시간 (부팅 후 ms)
|
||||
uint32_t timestamp;
|
||||
} sensor_data_t;
|
||||
|
||||
void init_status_monitor();
|
||||
esp_err_t update_sensor_period(int period);
|
||||
|
||||
#endif //ODROID_REMOTE_HTTP_MONITOR_H
|
||||
#endif // ODROID_REMOTE_HTTP_MONITOR_H
|
||||
|
||||
@@ -1,48 +1,82 @@
|
||||
#include "webserver.h"
|
||||
#include <stdlib.h>
|
||||
#include "auth.h"
|
||||
#include "cJSON.h"
|
||||
#include "climit.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_log.h"
|
||||
#include "nconfig.h"
|
||||
#include "wifi.h"
|
||||
#include "system.h"
|
||||
#include "esp_netif.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_timer.h"
|
||||
#include "monitor.h"
|
||||
#include "nconfig.h"
|
||||
#include "webserver.h"
|
||||
#include "wifi.h"
|
||||
|
||||
static const char *TAG = "webserver";
|
||||
static const char* TAG = "webserver";
|
||||
|
||||
static esp_err_t setting_get_handler(httpd_req_t *req)
|
||||
static esp_err_t setting_get_handler(httpd_req_t* req)
|
||||
{
|
||||
wifi_ap_record_t ap_info;
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
esp_err_t err = api_auth_check(req);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
char mode_buf[16];
|
||||
if (nconfig_read(WIFI_MODE, mode_buf, sizeof(mode_buf)) == ESP_OK) {
|
||||
cJSON_AddStringToObject(root, "mode", mode_buf);
|
||||
} else {
|
||||
wifi_ap_record_t ap_info;
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
|
||||
char buf[16];
|
||||
if (nconfig_read(WIFI_MODE, buf, sizeof(buf)) == ESP_OK)
|
||||
{
|
||||
cJSON_AddStringToObject(root, "mode", buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
cJSON_AddStringToObject(root, "mode", "sta"); // Default to sta
|
||||
}
|
||||
|
||||
char net_type_buf[16];
|
||||
if (nconfig_read(NETIF_TYPE, net_type_buf, sizeof(net_type_buf)) == ESP_OK) {
|
||||
cJSON_AddStringToObject(root, "net_type", net_type_buf);
|
||||
} else {
|
||||
if (nconfig_read(NETIF_TYPE, buf, sizeof(buf)) == ESP_OK)
|
||||
{
|
||||
cJSON_AddStringToObject(root, "net_type", buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
cJSON_AddStringToObject(root, "net_type", "dhcp"); // Default to dhcp
|
||||
}
|
||||
|
||||
// Add baudrate to the response
|
||||
char baud_buf[16];
|
||||
if (nconfig_read(UART_BAUD_RATE, baud_buf, sizeof(baud_buf)) == ESP_OK) {
|
||||
cJSON_AddStringToObject(root, "baudrate", baud_buf);
|
||||
if (nconfig_read(UART_BAUD_RATE, buf, sizeof(buf)) == ESP_OK)
|
||||
{
|
||||
cJSON_AddStringToObject(root, "baudrate", buf);
|
||||
}
|
||||
|
||||
if (wifi_get_current_ap_info(&ap_info) == ESP_OK) {
|
||||
if (nconfig_read(SENSOR_PERIOD_MS, buf, sizeof(buf)) == ESP_OK)
|
||||
{
|
||||
cJSON_AddStringToObject(root, "period", buf);
|
||||
}
|
||||
|
||||
// Add current limits to the response
|
||||
if (nconfig_read(VIN_CURRENT_LIMIT, buf, sizeof(buf)) == ESP_OK)
|
||||
{
|
||||
cJSON_AddNumberToObject(root, "vin_current_limit", atof(buf));
|
||||
}
|
||||
if (nconfig_read(MAIN_CURRENT_LIMIT, buf, sizeof(buf)) == ESP_OK)
|
||||
{
|
||||
cJSON_AddNumberToObject(root, "main_current_limit", atof(buf));
|
||||
}
|
||||
if (nconfig_read(USB_CURRENT_LIMIT, buf, sizeof(buf)) == ESP_OK)
|
||||
{
|
||||
cJSON_AddNumberToObject(root, "usb_current_limit", atof(buf));
|
||||
}
|
||||
|
||||
if (wifi_get_current_ap_info(&ap_info) == ESP_OK)
|
||||
{
|
||||
cJSON_AddBoolToObject(root, "connected", true);
|
||||
cJSON_AddStringToObject(root, "ssid", (const char *)ap_info.ssid);
|
||||
cJSON_AddStringToObject(root, "ssid", (const char*)ap_info.ssid);
|
||||
cJSON_AddNumberToObject(root, "rssi", ap_info.rssi);
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
cJSON* ip_obj = cJSON_CreateObject();
|
||||
if (wifi_get_current_ip_info(&ip_info) == ESP_OK) {
|
||||
if (wifi_get_current_ip_info(&ip_info) == ESP_OK)
|
||||
{
|
||||
char ip_str[16];
|
||||
esp_ip4addr_ntoa(&ip_info.ip, ip_str, sizeof(ip_str));
|
||||
cJSON_AddStringToObject(ip_obj, "ip", ip_str);
|
||||
@@ -54,21 +88,24 @@ static esp_err_t setting_get_handler(httpd_req_t *req)
|
||||
|
||||
esp_netif_dns_info_t dns_info;
|
||||
char dns_str[16];
|
||||
if (wifi_get_dns_info(ESP_NETIF_DNS_MAIN, &dns_info) == ESP_OK) {
|
||||
if (wifi_get_dns_info(ESP_NETIF_DNS_MAIN, &dns_info) == ESP_OK)
|
||||
{
|
||||
esp_ip4addr_ntoa(&dns_info.ip.u_addr.ip4, dns_str, sizeof(dns_str));
|
||||
cJSON_AddStringToObject(ip_obj, "dns1", dns_str);
|
||||
}
|
||||
if (wifi_get_dns_info(ESP_NETIF_DNS_BACKUP, &dns_info) == ESP_OK) {
|
||||
if (wifi_get_dns_info(ESP_NETIF_DNS_BACKUP, &dns_info) == ESP_OK)
|
||||
{
|
||||
esp_ip4addr_ntoa(&dns_info.ip.u_addr.ip4, dns_str, sizeof(dns_str));
|
||||
cJSON_AddStringToObject(ip_obj, "dns2", dns_str);
|
||||
}
|
||||
cJSON_AddItemToObject(root, "ip", ip_obj);
|
||||
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
cJSON_AddBoolToObject(root, "connected", false);
|
||||
}
|
||||
|
||||
const char *json_string = cJSON_Print(root);
|
||||
const char* json_string = cJSON_Print(root);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, json_string, HTTPD_RESP_USE_STRLEN);
|
||||
cJSON_Delete(root);
|
||||
@@ -77,18 +114,24 @@ static esp_err_t setting_get_handler(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t wifi_scan(httpd_req_t *req)
|
||||
static esp_err_t wifi_scan(httpd_req_t* req)
|
||||
{
|
||||
wifi_ap_record_t *ap_records;
|
||||
esp_err_t err = api_auth_check(req);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
wifi_ap_record_t* ap_records;
|
||||
uint16_t count;
|
||||
|
||||
wifi_scan_aps(&ap_records, &count);
|
||||
|
||||
cJSON *root = cJSON_CreateArray();
|
||||
cJSON* root = cJSON_CreateArray();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
cJSON *ap_obj = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(ap_obj, "ssid", (const char *)ap_records[i].ssid);
|
||||
cJSON* ap_obj = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(ap_obj, "ssid", (const char*)ap_records[i].ssid);
|
||||
cJSON_AddNumberToObject(ap_obj, "rssi", ap_records[i].rssi);
|
||||
cJSON_AddStringToObject(ap_obj, "authmode", auth_mode_str(ap_records[i].authmode));
|
||||
cJSON_AddItemToArray(root, ap_obj);
|
||||
@@ -98,7 +141,7 @@ static esp_err_t wifi_scan(httpd_req_t *req)
|
||||
free(ap_records);
|
||||
|
||||
|
||||
const char *json_string = cJSON_Print(root);
|
||||
const char* json_string = cJSON_Print(root);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, json_string, HTTPD_RESP_USE_STRLEN);
|
||||
cJSON_Delete(root);
|
||||
@@ -107,67 +150,96 @@ static esp_err_t wifi_scan(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t setting_post_handler(httpd_req_t *req)
|
||||
static esp_err_t setting_post_handler(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = api_auth_check(req);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
char buf[512];
|
||||
int received = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
|
||||
if (received <= 0) {
|
||||
if (received == HTTPD_SOCK_ERR_TIMEOUT) httpd_resp_send_408(req);
|
||||
if (received <= 0)
|
||||
{
|
||||
if (received == HTTPD_SOCK_ERR_TIMEOUT)
|
||||
httpd_resp_send_408(req);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[received] = '\0';
|
||||
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
if (root == NULL) {
|
||||
cJSON* root = cJSON_Parse(buf);
|
||||
if (root == NULL)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *mode_item = cJSON_GetObjectItem(root, "mode");
|
||||
cJSON *net_type_item = cJSON_GetObjectItem(root, "net_type");
|
||||
cJSON *ssid_item = cJSON_GetObjectItem(root, "ssid");
|
||||
cJSON *baud_item = cJSON_GetObjectItem(root, "baudrate");
|
||||
cJSON* mode_item = cJSON_GetObjectItem(root, "mode");
|
||||
cJSON* net_type_item = cJSON_GetObjectItem(root, "net_type");
|
||||
cJSON* ssid_item = cJSON_GetObjectItem(root, "ssid");
|
||||
cJSON* baud_item = cJSON_GetObjectItem(root, "baudrate");
|
||||
cJSON* period_item = cJSON_GetObjectItem(root, "period");
|
||||
cJSON* vin_climit_item = cJSON_GetObjectItem(root, "vin_current_limit");
|
||||
cJSON* main_climit_item = cJSON_GetObjectItem(root, "main_current_limit");
|
||||
cJSON* usb_climit_item = cJSON_GetObjectItem(root, "usb_current_limit");
|
||||
cJSON* new_username_item = cJSON_GetObjectItem(root, "new_username");
|
||||
cJSON* new_password_item = cJSON_GetObjectItem(root, "new_password");
|
||||
|
||||
if (mode_item && cJSON_IsString(mode_item)) {
|
||||
if (mode_item && cJSON_IsString(mode_item))
|
||||
{
|
||||
const char* mode = mode_item->valuestring;
|
||||
ESP_LOGI(TAG, "Received mode switch request: %s", mode);
|
||||
|
||||
if (strcmp(mode, "sta") == 0 || strcmp(mode, "apsta") == 0) {
|
||||
if (strcmp(mode, "apsta") == 0) {
|
||||
cJSON *ap_ssid_item = cJSON_GetObjectItem(root, "ap_ssid");
|
||||
cJSON *ap_pass_item = cJSON_GetObjectItem(root, "ap_password");
|
||||
if (strcmp(mode, "sta") == 0 || strcmp(mode, "apsta") == 0)
|
||||
{
|
||||
if (strcmp(mode, "apsta") == 0)
|
||||
{
|
||||
cJSON* ap_ssid_item = cJSON_GetObjectItem(root, "ap_ssid");
|
||||
cJSON* ap_pass_item = cJSON_GetObjectItem(root, "ap_password");
|
||||
|
||||
if (ap_ssid_item && cJSON_IsString(ap_ssid_item)) {
|
||||
if (ap_ssid_item && cJSON_IsString(ap_ssid_item))
|
||||
{
|
||||
nconfig_write(AP_SSID, ap_ssid_item->valuestring);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "AP SSID required for APSTA mode");
|
||||
cJSON_Delete(root);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (ap_pass_item && cJSON_IsString(ap_pass_item)) {
|
||||
if (ap_pass_item && cJSON_IsString(ap_pass_item))
|
||||
{
|
||||
nconfig_write(AP_PASSWORD, ap_pass_item->valuestring);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
nconfig_delete(AP_PASSWORD); // Open network
|
||||
}
|
||||
}
|
||||
|
||||
wifi_switch_mode(mode);
|
||||
httpd_resp_sendstr(req, "{\"status\":\"mode_switch_initiated\"}");
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid mode");
|
||||
}
|
||||
} else if (net_type_item && cJSON_IsString(net_type_item)) {
|
||||
}
|
||||
else if (net_type_item && cJSON_IsString(net_type_item))
|
||||
{
|
||||
const char* type = net_type_item->valuestring;
|
||||
ESP_LOGI(TAG, "Received network config: %s", type);
|
||||
|
||||
if (strcmp(type, "static") == 0) {
|
||||
cJSON *ip_item = cJSON_GetObjectItem(root, "ip");
|
||||
cJSON *gw_item = cJSON_GetObjectItem(root, "gateway");
|
||||
cJSON *sn_item = cJSON_GetObjectItem(root, "subnet");
|
||||
cJSON *d1_item = cJSON_GetObjectItem(root, "dns1");
|
||||
cJSON *d2_item = cJSON_GetObjectItem(root, "dns2");
|
||||
if (strcmp(type, "static") == 0)
|
||||
{
|
||||
cJSON* ip_item = cJSON_GetObjectItem(root, "ip");
|
||||
cJSON* gw_item = cJSON_GetObjectItem(root, "gateway");
|
||||
cJSON* sn_item = cJSON_GetObjectItem(root, "subnet");
|
||||
cJSON* d1_item = cJSON_GetObjectItem(root, "dns1");
|
||||
cJSON* d2_item = cJSON_GetObjectItem(root, "dns2");
|
||||
|
||||
const char* ip = cJSON_IsString(ip_item) ? ip_item->valuestring : NULL;
|
||||
const char* gw = cJSON_IsString(gw_item) ? gw_item->valuestring : NULL;
|
||||
@@ -175,45 +247,110 @@ static esp_err_t setting_post_handler(httpd_req_t *req)
|
||||
const char* d1 = cJSON_IsString(d1_item) ? d1_item->valuestring : NULL;
|
||||
const char* d2 = cJSON_IsString(d2_item) ? d2_item->valuestring : NULL;
|
||||
|
||||
if (ip && gw && sn && d1) {
|
||||
if (ip && gw && sn && d1)
|
||||
{
|
||||
nconfig_write(NETIF_TYPE, "static");
|
||||
nconfig_write(NETIF_IP, ip);
|
||||
nconfig_write(NETIF_GATEWAY, gw);
|
||||
nconfig_write(NETIF_SUBNET, sn);
|
||||
nconfig_write(NETIF_DNS1, d1);
|
||||
if (d2) nconfig_write(NETIF_DNS2, d2); else nconfig_delete(NETIF_DNS2);
|
||||
if (d2)
|
||||
nconfig_write(NETIF_DNS2, d2);
|
||||
else
|
||||
nconfig_delete(NETIF_DNS2);
|
||||
|
||||
wifi_use_static(ip, gw, sn, d1, d2);
|
||||
httpd_resp_sendstr(req, "{\"status\":\"static_config_applied\"}");
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing static IP fields");
|
||||
}
|
||||
} else if (strcmp(type, "dhcp") == 0) {
|
||||
}
|
||||
else if (strcmp(type, "dhcp") == 0)
|
||||
{
|
||||
nconfig_write(NETIF_TYPE, "dhcp");
|
||||
wifi_use_dhcp();
|
||||
httpd_resp_sendstr(req, "{\"status\":\"dhcp_config_applied\"}");
|
||||
}
|
||||
} else if (ssid_item && cJSON_IsString(ssid_item)) {
|
||||
cJSON *pass_item = cJSON_GetObjectItem(root, "password");
|
||||
if (cJSON_IsString(pass_item)) {
|
||||
nconfig_write(WIFI_SSID, ssid_item->valuestring);
|
||||
nconfig_write(WIFI_PASSWORD, pass_item->valuestring);
|
||||
nconfig_write(NETIF_TYPE, "dhcp"); // Default to DHCP on new connection
|
||||
|
||||
}
|
||||
else if (ssid_item && cJSON_IsString(ssid_item))
|
||||
{
|
||||
cJSON* pass_item = cJSON_GetObjectItem(root, "password");
|
||||
if (cJSON_IsString(pass_item))
|
||||
{
|
||||
httpd_resp_sendstr(req, "{\"status\":\"connection_initiated\"}");
|
||||
wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
wifi_connect();
|
||||
} else {
|
||||
|
||||
wifi_sta_set_ap(ssid_item->valuestring, pass_item->valuestring);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Password required");
|
||||
}
|
||||
} else if (baud_item && cJSON_IsString(baud_item)) {
|
||||
}
|
||||
else if (baud_item && cJSON_IsString(baud_item))
|
||||
{
|
||||
const char* baudrate = baud_item->valuestring;
|
||||
ESP_LOGI(TAG, "Received baudrate set request: %s", baudrate);
|
||||
nconfig_write(UART_BAUD_RATE, baudrate);
|
||||
change_baud_rate(strtol(baudrate, NULL, 10));
|
||||
httpd_resp_sendstr(req, "{\"status\":\"baudrate_updated\"}");
|
||||
} else {
|
||||
}
|
||||
else if (period_item && cJSON_IsString(period_item))
|
||||
{
|
||||
const char* period_str = period_item->valuestring;
|
||||
ESP_LOGI(TAG, "Received period set request: %s", period_str);
|
||||
update_sensor_period(strtol(period_str, NULL, 10));
|
||||
httpd_resp_sendstr(req, "{\"status\":\"period_updated\"}");
|
||||
}
|
||||
else if (vin_climit_item || main_climit_item || usb_climit_item)
|
||||
{
|
||||
char num_buf[10];
|
||||
if (vin_climit_item && cJSON_IsNumber(vin_climit_item))
|
||||
{
|
||||
double val = vin_climit_item->valuedouble;
|
||||
if (val >= 0.0 && val <= VIN_CURRENT_LIMIT_MAX)
|
||||
{
|
||||
snprintf(num_buf, sizeof(num_buf), "%.2f", val);
|
||||
nconfig_write(VIN_CURRENT_LIMIT, num_buf);
|
||||
climit_set_vin(val);
|
||||
}
|
||||
}
|
||||
if (main_climit_item && cJSON_IsNumber(main_climit_item))
|
||||
{
|
||||
double val = main_climit_item->valuedouble;
|
||||
if (val >= 0.0 && val <= MAIN_CURRENT_LIMIT_MAX)
|
||||
{
|
||||
snprintf(num_buf, sizeof(num_buf), "%.2f", val);
|
||||
nconfig_write(MAIN_CURRENT_LIMIT, num_buf);
|
||||
climit_set_main(val);
|
||||
}
|
||||
}
|
||||
if (usb_climit_item && cJSON_IsNumber(usb_climit_item))
|
||||
{
|
||||
double val = usb_climit_item->valuedouble;
|
||||
if (val >= 0.0 && val <= USB_CURRENT_LIMIT_MAX)
|
||||
{
|
||||
snprintf(num_buf, sizeof(num_buf), "%.2f", val);
|
||||
nconfig_write(USB_CURRENT_LIMIT, num_buf);
|
||||
climit_set_usb(val);
|
||||
}
|
||||
}
|
||||
httpd_resp_sendstr(req, "{\"status\":\"current_limit_updated\"}");
|
||||
}
|
||||
else if (new_username_item && cJSON_IsString(new_username_item) && new_password_item &&
|
||||
cJSON_IsString(new_password_item))
|
||||
{
|
||||
const char* new_username = new_username_item->valuestring;
|
||||
const char* new_password = new_password_item->valuestring;
|
||||
|
||||
nconfig_write(PAGE_USERNAME, new_username);
|
||||
nconfig_write(PAGE_PASSWORD, new_password);
|
||||
ESP_LOGI(TAG, "Username and password updated successfully.");
|
||||
httpd_resp_sendstr(req, "{\"status\":\"user_credentials_updated\"}");
|
||||
}
|
||||
else
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid payload");
|
||||
}
|
||||
|
||||
@@ -223,27 +360,12 @@ static esp_err_t setting_post_handler(httpd_req_t *req)
|
||||
|
||||
void register_wifi_endpoint(httpd_handle_t server)
|
||||
{
|
||||
httpd_uri_t status = {
|
||||
.uri = "/api/setting",
|
||||
.method = HTTP_GET,
|
||||
.handler = setting_get_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_uri_t status = {.uri = "/api/setting", .method = HTTP_GET, .handler = setting_get_handler, .user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &status);
|
||||
|
||||
httpd_uri_t set = {
|
||||
.uri = "/api/setting",
|
||||
.method = HTTP_POST,
|
||||
.handler = setting_post_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_uri_t set = {.uri = "/api/setting", .method = HTTP_POST, .handler = setting_post_handler, .user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &set);
|
||||
|
||||
httpd_uri_t scan = {
|
||||
.uri = "/api/wifi/scan",
|
||||
.method = HTTP_GET,
|
||||
.handler = wifi_scan,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_uri_t scan = {.uri = "/api/wifi/scan", .method = HTTP_GET, .handler = wifi_scan, .user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &scan);
|
||||
}
|
||||
|
||||
183
main/service/sw.c
Normal file
183
main/service/sw.c
Normal file
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// Created by vl011 on 2025-08-28.
|
||||
//
|
||||
|
||||
#include "sw.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <ina3221.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "pb.h"
|
||||
#include "pb_encode.h"
|
||||
#include "pca9557.h"
|
||||
#include "status.pb.h"
|
||||
#include "webserver.h"
|
||||
|
||||
#define I2C_PORT 0
|
||||
|
||||
#define GPIO_SDA CONFIG_I2C_GPIO_SDA
|
||||
#define GPIO_SCL CONFIG_I2C_GPIO_SCL
|
||||
#define GPIO_MAIN CONFIG_EXPANDER_GPIO_SW_12V
|
||||
#define GPIO_USB CONFIG_EXPANDER_GPIO_SW_5V
|
||||
#define GPIO_PWR CONFIG_EXPANDER_GPIO_TRIGGER_POWER
|
||||
#define GPIO_RST CONFIG_EXPANDER_GPIO_TRIGGER_RESET
|
||||
|
||||
#define POWER_DELAY (CONFIG_TRIGGER_POWER_DELAY_MS * 1000)
|
||||
#define RESET_DELAY (CONFIG_TRIGGER_RESET_DELAY_MS * 1000)
|
||||
|
||||
#define PB_BUFFER_SIZE 256
|
||||
|
||||
static const char* TAG = "control";
|
||||
|
||||
static bool load_switch_12v_status = false;
|
||||
static bool load_switch_5v_status = false;
|
||||
|
||||
static SemaphoreHandle_t expander_mutex;
|
||||
#define MUTEX_TIMEOUT (pdMS_TO_TICKS(100))
|
||||
|
||||
static i2c_dev_t pca = {0};
|
||||
|
||||
static esp_timer_handle_t power_trigger_timer;
|
||||
static esp_timer_handle_t reset_trigger_timer;
|
||||
|
||||
static void send_sw_status_message()
|
||||
{
|
||||
StatusMessage message = StatusMessage_init_zero;
|
||||
message.which_payload = StatusMessage_sw_status_tag;
|
||||
LoadSwStatus* sw_status = &message.payload.sw_status;
|
||||
|
||||
sw_status->main = load_switch_12v_status;
|
||||
sw_status->usb = load_switch_5v_status;
|
||||
|
||||
uint8_t buffer[PB_BUFFER_SIZE];
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
|
||||
|
||||
if (!pb_encode(&stream, StatusMessage_fields, &message))
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to encode protobuf message: %s", PB_GET_ERROR(&stream));
|
||||
return;
|
||||
}
|
||||
|
||||
push_data_to_ws(buffer, stream.bytes_written);
|
||||
}
|
||||
|
||||
|
||||
static void trigger_off_callback(void* arg)
|
||||
{
|
||||
if (xSemaphoreTake(expander_mutex, MUTEX_TIMEOUT) == pdFALSE)
|
||||
{
|
||||
ESP_LOGW(TAG, "Control error");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t gpio_pin = (int)arg;
|
||||
pca9557_set_level(&pca, gpio_pin, 1);
|
||||
xSemaphoreGive(expander_mutex);
|
||||
}
|
||||
|
||||
void config_sw()
|
||||
{
|
||||
ESP_ERROR_CHECK(pca9557_set_mode(&pca, GPIO_MAIN, PCA9557_MODE_OUTPUT));
|
||||
ESP_ERROR_CHECK(pca9557_set_mode(&pca, GPIO_USB, PCA9557_MODE_OUTPUT));
|
||||
ESP_ERROR_CHECK(pca9557_set_mode(&pca, GPIO_PWR, PCA9557_MODE_OUTPUT));
|
||||
ESP_ERROR_CHECK(pca9557_set_mode(&pca, GPIO_RST, PCA9557_MODE_OUTPUT));
|
||||
|
||||
ESP_ERROR_CHECK(pca9557_set_level(&pca, GPIO_PWR, 1));
|
||||
ESP_ERROR_CHECK(pca9557_set_level(&pca, GPIO_RST, 1));
|
||||
|
||||
uint32_t val = 0;
|
||||
ESP_ERROR_CHECK(pca9557_get_level(&pca, CONFIG_EXPANDER_GPIO_SW_12V, &val));
|
||||
load_switch_12v_status = val != 0 ? true : false;
|
||||
ESP_ERROR_CHECK(pca9557_get_level(&pca, CONFIG_EXPANDER_GPIO_SW_5V, &val));
|
||||
load_switch_5v_status = val != 0 ? true : false;
|
||||
|
||||
send_sw_status_message();
|
||||
}
|
||||
|
||||
void init_sw()
|
||||
{
|
||||
ESP_ERROR_CHECK(pca9557_init_desc(&pca, 0x18, I2C_PORT, GPIO_SDA, GPIO_SCL));
|
||||
|
||||
config_sw();
|
||||
|
||||
const esp_timer_create_args_t power_timer_args = {
|
||||
.callback = &trigger_off_callback, .arg = (void*)GPIO_PWR, .name = "power_trigger_off"};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&power_timer_args, &power_trigger_timer));
|
||||
|
||||
const esp_timer_create_args_t reset_timer_args = {
|
||||
.callback = &trigger_off_callback, .arg = (void*)GPIO_RST, .name = "power_trigger_off"};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&reset_timer_args, &reset_trigger_timer));
|
||||
|
||||
expander_mutex = xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
void trig_power()
|
||||
{
|
||||
ESP_LOGI(TAG, "Trig power");
|
||||
if (xSemaphoreTake(expander_mutex, MUTEX_TIMEOUT) == pdFALSE)
|
||||
{
|
||||
ESP_LOGW(TAG, "Control error");
|
||||
return;
|
||||
}
|
||||
pca9557_set_level(&pca, GPIO_PWR, 0);
|
||||
xSemaphoreGive(expander_mutex);
|
||||
esp_timer_stop(power_trigger_timer);
|
||||
esp_timer_start_once(power_trigger_timer, POWER_DELAY);
|
||||
}
|
||||
|
||||
void trig_reset()
|
||||
{
|
||||
ESP_LOGI(TAG, "Trig reset");
|
||||
if (xSemaphoreTake(expander_mutex, MUTEX_TIMEOUT) == pdFALSE)
|
||||
{
|
||||
ESP_LOGW(TAG, "Control error");
|
||||
return;
|
||||
}
|
||||
pca9557_set_level(&pca, GPIO_RST, 0);
|
||||
xSemaphoreGive(expander_mutex);
|
||||
esp_timer_stop(reset_trigger_timer);
|
||||
esp_timer_start_once(reset_trigger_timer, RESET_DELAY);
|
||||
}
|
||||
|
||||
void set_main_load_switch(bool on)
|
||||
{
|
||||
ESP_LOGI(TAG, "Set main load switch to %s", on ? "on" : "off");
|
||||
if (load_switch_12v_status == on)
|
||||
return;
|
||||
if (xSemaphoreTake(expander_mutex, MUTEX_TIMEOUT) == pdFALSE)
|
||||
{
|
||||
ESP_LOGW(TAG, "Control error");
|
||||
return;
|
||||
}
|
||||
pca9557_set_level(&pca, GPIO_MAIN, on);
|
||||
load_switch_12v_status = on;
|
||||
xSemaphoreGive(expander_mutex);
|
||||
send_sw_status_message();
|
||||
}
|
||||
|
||||
void set_usb_load_switch(bool on)
|
||||
{
|
||||
ESP_LOGI(TAG, "Set usb load switch to %s", on ? "on" : "off");
|
||||
if (load_switch_5v_status == on)
|
||||
return;
|
||||
if (xSemaphoreTake(expander_mutex, MUTEX_TIMEOUT) == pdFALSE)
|
||||
{
|
||||
ESP_LOGW(TAG, "Control error");
|
||||
return;
|
||||
}
|
||||
pca9557_set_level(&pca, GPIO_USB, on);
|
||||
load_switch_5v_status = on;
|
||||
xSemaphoreGive(expander_mutex);
|
||||
send_sw_status_message();
|
||||
}
|
||||
|
||||
bool get_main_load_switch() { return load_switch_12v_status; }
|
||||
|
||||
bool get_usb_load_switch() { return load_switch_5v_status; }
|
||||
18
main/service/sw.h
Normal file
18
main/service/sw.h
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Created by vl011 on 2025-08-28.
|
||||
//
|
||||
|
||||
#ifndef ODROID_POWER_MATE_SW_H
|
||||
#define ODROID_POWER_MATE_SW_H
|
||||
#include <stdbool.h>
|
||||
|
||||
void init_sw();
|
||||
void config_sw();
|
||||
void trig_power();
|
||||
void trig_reset();
|
||||
void set_main_load_switch(bool on);
|
||||
void set_usb_load_switch(bool on);
|
||||
bool get_main_load_switch();
|
||||
bool get_usb_load_switch();
|
||||
|
||||
#endif // ODROID_POWER_MATE_SW_H
|
||||
108
main/service/system.c
Normal file
108
main/service/system.c
Normal file
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// Created by shinys on 25. 8. 5.
|
||||
//
|
||||
|
||||
#include <system.h>
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_timer.h>
|
||||
#include <string.h>
|
||||
#include "auth.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
static const char* TAG = "odroid";
|
||||
|
||||
static esp_timer_handle_t reboot_timer_handle = NULL;
|
||||
|
||||
static void reboot_timer_callback(void* arg)
|
||||
{
|
||||
ESP_LOGI(TAG, "Rebooting now...");
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
void start_reboot_timer(int sec)
|
||||
{
|
||||
if (reboot_timer_handle != NULL)
|
||||
{
|
||||
ESP_LOGW(TAG, "The reboot timer is already running.");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Device will reboot in %d seconds.", sec);
|
||||
|
||||
const esp_timer_create_args_t reboot_timer_args = {.callback = &reboot_timer_callback, .name = "reboot-timer"};
|
||||
|
||||
if (esp_timer_create(&reboot_timer_args, &reboot_timer_handle) != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to create reboot timer.");
|
||||
reboot_timer_handle = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (esp_timer_start_once(reboot_timer_handle, (uint64_t)sec * 1000000) != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to start reboot timer.");
|
||||
esp_timer_delete(reboot_timer_handle);
|
||||
reboot_timer_handle = NULL;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t reboot_post_handler(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = api_auth_check(req);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
const char* resp_str = "{\"status\": \"reboot timer started\"}";
|
||||
httpd_resp_send(req, resp_str, strlen(resp_str));
|
||||
|
||||
start_reboot_timer(3);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void stop_reboot_timer()
|
||||
{
|
||||
if (reboot_timer_handle == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
esp_timer_stop(reboot_timer_handle);
|
||||
esp_timer_delete(reboot_timer_handle);
|
||||
reboot_timer_handle = NULL;
|
||||
ESP_LOGI(TAG, "Reboot timer stopped.");
|
||||
}
|
||||
|
||||
void register_reboot_endpoint(httpd_handle_t server)
|
||||
{
|
||||
httpd_uri_t post_uri = {
|
||||
.uri = "/api/reboot", .method = HTTP_POST, .handler = reboot_post_handler, .user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &post_uri);
|
||||
}
|
||||
|
||||
static esp_err_t version_get_handler(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = api_auth_check(req);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
char buf[100];
|
||||
sprintf(buf, "{\"version\": \"%s-%s\"}", VERSION_TAG, VERSION_HASH);
|
||||
httpd_resp_send(req, buf, strlen(buf));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void register_version_endpoint(httpd_handle_t server)
|
||||
{
|
||||
httpd_uri_t post_uri = {
|
||||
.uri = "/api/version", .method = HTTP_GET, .handler = version_get_handler, .user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &post_uri);
|
||||
}
|
||||
@@ -1,92 +1,199 @@
|
||||
#include "webserver.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "auth.h"
|
||||
#include "cJSON.h"
|
||||
#include "dbg_console.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "nconfig.h"
|
||||
#include "monitor.h"
|
||||
#include "datalog.h"
|
||||
|
||||
#include "lwip/err.h"
|
||||
#include "lwip/sys.h"
|
||||
#include "monitor.h"
|
||||
#include "nconfig.h"
|
||||
#include "system.h"
|
||||
|
||||
static const char *TAG = "WEBSERVER";
|
||||
static const char* TAG = "WEBSERVER";
|
||||
|
||||
static esp_err_t index_handler(httpd_req_t *req) {
|
||||
static esp_err_t index_handler(httpd_req_t* req)
|
||||
{
|
||||
extern const unsigned char index_html_start[] asm("_binary_index_html_gz_start");
|
||||
extern const unsigned char index_html_end[] asm("_binary_index_html_gz_end");
|
||||
const size_t index_html_size = (index_html_end - index_html_start);
|
||||
|
||||
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
|
||||
httpd_resp_set_hdr(req, "Cache-Control", "max-age=3600");
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_send(req, (const char *)index_html_start, index_html_size);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t datalog_download_handler(httpd_req_t *req)
|
||||
{
|
||||
const char *filepath = datalog_get_path();
|
||||
FILE *f = fopen(filepath, "r");
|
||||
if (f == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to open datalog file for reading");
|
||||
httpd_resp_send_404(req);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "text/csv");
|
||||
httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"datalog.csv\"");
|
||||
|
||||
char buffer[1024];
|
||||
size_t bytes_read;
|
||||
while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) > 0) {
|
||||
if (httpd_resp_send_chunk(req, buffer, bytes_read) != ESP_OK) {
|
||||
size_t remaining = index_html_size;
|
||||
const char* ptr = (const char*)index_html_start;
|
||||
while (remaining > 0)
|
||||
{
|
||||
size_t to_send = remaining < 2048 ? remaining : 2048;
|
||||
if (httpd_resp_send_chunk(req, ptr, to_send) != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "File sending failed!");
|
||||
fclose(f);
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
httpd_resp_send_500(req);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ptr += to_send;
|
||||
remaining -= to_send;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// HTTP 서버 시작
|
||||
void start_webserver(void) {
|
||||
static esp_err_t login_handler(httpd_req_t* req)
|
||||
{
|
||||
char content[100]; // Adjust size as needed for username/password
|
||||
int ret = httpd_req_recv(req, content, sizeof(content) - 1); // -1 for null terminator
|
||||
if (ret <= 0)
|
||||
{ // 0 means connection closed, < 0 means error
|
||||
if (ret == HTTPD_SOCK_ERR_TIMEOUT)
|
||||
{
|
||||
httpd_resp_send_408(req);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
content[ret] = '\0'; // Null-terminate the received data
|
||||
|
||||
ESP_LOGI(TAG, "Received login request: %s", content);
|
||||
|
||||
cJSON* root = cJSON_Parse(content);
|
||||
if (root == NULL)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON* username_json = cJSON_GetObjectItemCaseSensitive(root, "username");
|
||||
cJSON* password_json = cJSON_GetObjectItemCaseSensitive(root, "password");
|
||||
|
||||
if (!cJSON_IsString(username_json) || (username_json->valuestring == NULL) || !cJSON_IsString(password_json) ||
|
||||
(password_json->valuestring == NULL))
|
||||
{
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing username or password");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
const char* received_username = username_json->valuestring;
|
||||
const char* received_password = password_json->valuestring;
|
||||
|
||||
// Get stored username and password from nconfig
|
||||
size_t stored_username_len = 0;
|
||||
size_t stored_password_len = 0;
|
||||
char* stored_username = NULL;
|
||||
char* stored_password = NULL;
|
||||
bool credentials_match = false;
|
||||
|
||||
if (nconfig_get_str_len(PAGE_USERNAME, &stored_username_len) == ESP_OK && stored_username_len > 1)
|
||||
{
|
||||
stored_username = (char*)malloc(stored_username_len);
|
||||
if (stored_username)
|
||||
{
|
||||
if (nconfig_read(PAGE_USERNAME, stored_username, stored_username_len) != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to read stored username from nconfig");
|
||||
free(stored_username);
|
||||
stored_username = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nconfig_get_str_len(PAGE_PASSWORD, &stored_password_len) == ESP_OK && stored_password_len > 1)
|
||||
{
|
||||
stored_password = (char*)malloc(stored_password_len);
|
||||
if (stored_password)
|
||||
{
|
||||
if (nconfig_read(PAGE_PASSWORD, stored_password, stored_password_len) != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to read stored password from nconfig");
|
||||
free(stored_password);
|
||||
stored_password = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stored_username && stored_password)
|
||||
{
|
||||
if (strcmp(received_username, stored_username) == 0 && strcmp(received_password, stored_password) == 0)
|
||||
{
|
||||
credentials_match = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (stored_username)
|
||||
free(stored_username);
|
||||
if (stored_password)
|
||||
free(stored_password);
|
||||
|
||||
if (credentials_match)
|
||||
{
|
||||
char* token = auth_generate_token();
|
||||
if (token)
|
||||
{
|
||||
cJSON* response_root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response_root, "token", token);
|
||||
char* json_response = cJSON_Print(response_root);
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_sendstr(req, json_response);
|
||||
|
||||
free(token); // Free the token generated by auth_generate_token
|
||||
free(json_response);
|
||||
cJSON_Delete(response_root);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to generate token");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Invalid credentials");
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
void start_webserver(void)
|
||||
{
|
||||
auth_init();
|
||||
|
||||
httpd_handle_t server = NULL;
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.stack_size = 1024 * 8;
|
||||
config.max_uri_handlers = 10;
|
||||
config.task_priority = 12;
|
||||
config.max_open_sockets = 7;
|
||||
|
||||
if (httpd_start(&server, &config) != ESP_OK) {
|
||||
return ;
|
||||
if (httpd_start(&server, &config) != ESP_OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Index page
|
||||
httpd_uri_t index = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = index_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_uri_t index = {.uri = "/", .method = HTTP_GET, .handler = index_handler, .user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &index);
|
||||
|
||||
httpd_uri_t datalog_uri = {
|
||||
.uri = "/datalog.csv",
|
||||
.method = HTTP_GET,
|
||||
.handler = datalog_download_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(server, &datalog_uri);
|
||||
// Login endpoint
|
||||
httpd_uri_t login = {.uri = "/login", .method = HTTP_POST, .handler = login_handler, .user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &login);
|
||||
|
||||
register_wifi_endpoint(server);
|
||||
register_ws_endpoint(server);
|
||||
register_control_endpoint(server);
|
||||
register_reboot_endpoint(server);
|
||||
register_version_endpoint(server);
|
||||
|
||||
init_status_monitor();
|
||||
|
||||
initialize_dbg_console();
|
||||
}
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
|
||||
#ifndef ODROID_REMOTE_HTTP_WEBSERVER_H
|
||||
#define ODROID_REMOTE_HTTP_WEBSERVER_H
|
||||
#include "cJSON.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_http_server.h"
|
||||
#include "system.h"
|
||||
|
||||
void register_wifi_endpoint(httpd_handle_t server);
|
||||
void register_ws_endpoint(httpd_handle_t server);
|
||||
void register_control_endpoint(httpd_handle_t server);
|
||||
void push_data_to_ws(cJSON *data);
|
||||
void push_data_to_ws(const uint8_t* data, size_t len);
|
||||
void register_reboot_endpoint(httpd_handle_t server);
|
||||
esp_err_t change_baud_rate(int baud_rate);
|
||||
void register_version_endpoint(httpd_handle_t server);
|
||||
|
||||
#endif //ODROID_REMOTE_HTTP_WEBSERVER_H
|
||||
#endif // ODROID_REMOTE_HTTP_WEBSERVER_H
|
||||
|
||||
@@ -2,28 +2,31 @@
|
||||
// Created by shinys on 25. 8. 18..
|
||||
//
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "webserver.h"
|
||||
#include "auth.h"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_log.h"
|
||||
#include "nconfig.h"
|
||||
#include "driver/uart.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "nconfig.h"
|
||||
#include "pb.h"
|
||||
#include "pb_encode.h"
|
||||
#include "status.pb.h"
|
||||
#include "string.h" // Added for strlen and strncmp
|
||||
#include "webserver.h"
|
||||
|
||||
#define UART_NUM UART_NUM_1
|
||||
#define BUF_SIZE (2048)
|
||||
#define UART_TX_PIN CONFIG_GPIO_UART_TX
|
||||
#define UART_RX_PIN CONFIG_GPIO_UART_RX
|
||||
#define CHUNK_SIZE (1024)
|
||||
#define CHUNK_SIZE (2048)
|
||||
#define PB_UART_BUFFER_SIZE (CHUNK_SIZE + 64)
|
||||
|
||||
static const char *TAG = "ws-uart";
|
||||
static const char* TAG = "ws-uart";
|
||||
|
||||
static int client_fd = -1;
|
||||
static SemaphoreHandle_t client_fd_mutex;
|
||||
|
||||
// Unified message structure for the websocket queue
|
||||
enum ws_message_type {
|
||||
enum ws_message_type
|
||||
{
|
||||
WS_MSG_STATUS,
|
||||
WS_MSG_UART
|
||||
};
|
||||
@@ -31,237 +34,272 @@ enum ws_message_type {
|
||||
struct ws_message
|
||||
{
|
||||
enum ws_message_type type;
|
||||
union {
|
||||
struct {
|
||||
cJSON *data;
|
||||
} status;
|
||||
struct {
|
||||
uint8_t *data;
|
||||
size_t len;
|
||||
} uart;
|
||||
} content;
|
||||
uint8_t* data;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
static QueueHandle_t ws_queue;
|
||||
struct bytes_arg
|
||||
{
|
||||
const void* data;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
// Unified task to send data from the queue to the websocket client
|
||||
static void unified_ws_sender_task(void *arg)
|
||||
#define MAX_CLIENT 7
|
||||
static QueueHandle_t ws_queue;
|
||||
static QueueHandle_t uart_event_queue;
|
||||
static int client_fds[MAX_CLIENT];
|
||||
|
||||
static bool encode_bytes_callback(pb_ostream_t* stream, const pb_field_t* field, void* const* arg)
|
||||
{
|
||||
struct bytes_arg* br = (struct bytes_arg*)(*arg);
|
||||
if (!pb_encode_tag_for_field(stream, field))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return pb_encode_string(stream, (uint8_t*)br->data, br->len);
|
||||
}
|
||||
|
||||
static void unified_ws_sender_task(void* arg)
|
||||
{
|
||||
httpd_handle_t server = (httpd_handle_t)arg;
|
||||
struct ws_message msg;
|
||||
const TickType_t PING_INTERVAL = pdMS_TO_TICKS(5000);
|
||||
|
||||
while (1) {
|
||||
if (xQueueReceive(ws_queue, &msg, PING_INTERVAL)) {
|
||||
xSemaphoreTake(client_fd_mutex, portMAX_DELAY);
|
||||
int fd = client_fd;
|
||||
while (1)
|
||||
{
|
||||
if (xQueueReceive(ws_queue, &msg, portMAX_DELAY))
|
||||
{
|
||||
size_t clients = MAX_CLIENT;
|
||||
if (httpd_get_client_list(server, &clients, client_fds) != ESP_OK)
|
||||
{
|
||||
free(msg.data);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fd <= 0) {
|
||||
xSemaphoreGive(client_fd_mutex);
|
||||
// Free memory if client is not connected
|
||||
if (msg.type == WS_MSG_STATUS) {
|
||||
cJSON_Delete(msg.content.status.data);
|
||||
} else {
|
||||
free(msg.content.uart.data);
|
||||
}
|
||||
if (clients == 0)
|
||||
{
|
||||
free(msg.data);
|
||||
continue;
|
||||
}
|
||||
|
||||
httpd_ws_frame_t ws_pkt = {0};
|
||||
esp_err_t err = ESP_FAIL;
|
||||
ws_pkt.payload = msg.data;
|
||||
ws_pkt.len = msg.len;
|
||||
ws_pkt.type = HTTPD_WS_TYPE_BINARY;
|
||||
|
||||
if (msg.type == WS_MSG_STATUS) {
|
||||
char *json_string = cJSON_Print(msg.content.status.data);
|
||||
cJSON_Delete(msg.content.status.data);
|
||||
|
||||
ws_pkt.payload = (uint8_t *)json_string;
|
||||
ws_pkt.len = strlen(json_string);
|
||||
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
|
||||
err = httpd_ws_send_frame_async(server, fd, &ws_pkt);
|
||||
free(json_string);
|
||||
|
||||
} else { // WS_MSG_UART
|
||||
ws_pkt.payload = msg.content.uart.data;
|
||||
ws_pkt.len = msg.content.uart.len;
|
||||
ws_pkt.type = HTTPD_WS_TYPE_BINARY;
|
||||
err = httpd_ws_send_frame_async(server, fd, &ws_pkt);
|
||||
free(msg.content.uart.data);
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "unified_ws_sender_task: async send failed for fd %d, error: %s", fd, esp_err_to_name(err));
|
||||
client_fd = -1;
|
||||
}
|
||||
|
||||
xSemaphoreGive(client_fd_mutex);
|
||||
|
||||
} else {
|
||||
// Queue receive timed out, send a PING to keep connection alive
|
||||
xSemaphoreTake(client_fd_mutex, portMAX_DELAY);
|
||||
int fd = client_fd;
|
||||
if (fd > 0) {
|
||||
httpd_ws_frame_t ping_pkt = {0};
|
||||
ping_pkt.type = HTTPD_WS_TYPE_PING;
|
||||
ping_pkt.final = true;
|
||||
esp_err_t err = httpd_ws_send_frame_async(server, fd, &ping_pkt);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to send PING frame, closing connection for fd %d, error: %s", fd, esp_err_to_name(err));
|
||||
client_fd = -1;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(client_fd_mutex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void uart_polling_task(void *arg)
|
||||
{
|
||||
static uint8_t data_buf[BUF_SIZE];
|
||||
const TickType_t MIN_POLLING_INTERVAL = pdMS_TO_TICKS(1);
|
||||
const TickType_t MAX_POLLING_INTERVAL = pdMS_TO_TICKS(10);
|
||||
const TickType_t READ_TIMEOUT = pdMS_TO_TICKS(5);
|
||||
|
||||
TickType_t current_interval = MIN_POLLING_INTERVAL;
|
||||
int consecutive_empty_polls = 0;
|
||||
int cached_client_fd = -1;
|
||||
TickType_t last_client_check = 0;
|
||||
const TickType_t CLIENT_CHECK_INTERVAL = pdMS_TO_TICKS(100);
|
||||
|
||||
while(1) {
|
||||
TickType_t current_time = xTaskGetTickCount();
|
||||
|
||||
if (current_time - last_client_check >= CLIENT_CHECK_INTERVAL) {
|
||||
xSemaphoreTake(client_fd_mutex, portMAX_DELAY);
|
||||
cached_client_fd = client_fd;
|
||||
xSemaphoreGive(client_fd_mutex);
|
||||
last_client_check = current_time;
|
||||
}
|
||||
|
||||
size_t available_len;
|
||||
esp_err_t err = uart_get_buffered_data_len(UART_NUM, &available_len);
|
||||
|
||||
if (err != ESP_OK || available_len == 0) {
|
||||
consecutive_empty_polls++;
|
||||
if (consecutive_empty_polls > 5) {
|
||||
current_interval = MAX_POLLING_INTERVAL;
|
||||
} else if (consecutive_empty_polls > 2) {
|
||||
current_interval = pdMS_TO_TICKS(5);
|
||||
}
|
||||
|
||||
if (cached_client_fd <= 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
continue;
|
||||
}
|
||||
|
||||
vTaskDelay(current_interval);
|
||||
continue;
|
||||
}
|
||||
|
||||
consecutive_empty_polls = 0;
|
||||
current_interval = MIN_POLLING_INTERVAL;
|
||||
|
||||
if (cached_client_fd <= 0) {
|
||||
uart_flush_input(UART_NUM);
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t total_processed = 0;
|
||||
while (available_len > 0 && total_processed < BUF_SIZE) {
|
||||
size_t read_size = (available_len > (BUF_SIZE - total_processed)) ?
|
||||
(BUF_SIZE - total_processed) : available_len;
|
||||
|
||||
int bytes_read = uart_read_bytes(UART_NUM, data_buf + total_processed,
|
||||
read_size, READ_TIMEOUT);
|
||||
|
||||
if (bytes_read <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
total_processed += bytes_read;
|
||||
available_len -= bytes_read;
|
||||
|
||||
uart_get_buffered_data_len(UART_NUM, &available_len);
|
||||
}
|
||||
|
||||
if (total_processed > 0) {
|
||||
size_t offset = 0;
|
||||
|
||||
while (offset < total_processed) {
|
||||
const size_t chunk_size = (total_processed - offset > CHUNK_SIZE) ?
|
||||
CHUNK_SIZE : (total_processed - offset);
|
||||
|
||||
struct ws_message msg;
|
||||
msg.type = WS_MSG_UART;
|
||||
msg.content.uart.data = malloc(chunk_size);
|
||||
if (!msg.content.uart.data) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for uart ws msg");
|
||||
break;
|
||||
}
|
||||
|
||||
memcpy(msg.content.uart.data, data_buf + offset, chunk_size);
|
||||
msg.content.uart.len = chunk_size;
|
||||
|
||||
if (xQueueSend(ws_queue, &msg, 0) != pdPASS) {
|
||||
if (xQueueSend(ws_queue, &msg, pdMS_TO_TICKS(5)) != pdPASS) {
|
||||
ESP_LOGW(TAG, "ws sender queue full, dropping %zu bytes", chunk_size);
|
||||
free(msg.content.uart.data);
|
||||
for (size_t i = 0; i < clients; ++i)
|
||||
{
|
||||
int fd = client_fds[i];
|
||||
if (httpd_ws_get_fd_info(server, fd) == HTTPD_WS_CLIENT_WEBSOCKET)
|
||||
{
|
||||
esp_err_t err = httpd_ws_send_frame_async(server, fd, &ws_pkt);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "unified_ws_sender_task: async send failed for fd %d, error: %s", fd,
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
offset += chunk_size;
|
||||
}
|
||||
}
|
||||
|
||||
if (available_len > 0) {
|
||||
vTaskDelay(MIN_POLLING_INTERVAL);
|
||||
} else {
|
||||
vTaskDelay(current_interval);
|
||||
free(msg.data);
|
||||
}
|
||||
}
|
||||
|
||||
free(client_fds);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static esp_err_t ws_handler(httpd_req_t *req) {
|
||||
if (req->method == HTTP_GET) {
|
||||
xSemaphoreTake(client_fd_mutex, portMAX_DELAY);
|
||||
if (client_fd > 0) {
|
||||
// A client is already connected. Reject the new connection.
|
||||
ESP_LOGW(TAG, "Another client tried to connect, but a session is already active. Rejecting.");
|
||||
xSemaphoreGive(client_fd_mutex);
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "Another client is already connected");
|
||||
static void uart_polling_task(void* arg)
|
||||
{
|
||||
static uint8_t data_buf[BUF_SIZE];
|
||||
static uint8_t pb_buffer[PB_UART_BUFFER_SIZE];
|
||||
|
||||
while (1)
|
||||
{
|
||||
size_t available_len;
|
||||
uart_get_buffered_data_len(UART_NUM, &available_len);
|
||||
|
||||
if (available_len == 0)
|
||||
{
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t read_len = (available_len > BUF_SIZE) ? BUF_SIZE : available_len;
|
||||
int bytes_read = uart_read_bytes(UART_NUM, data_buf, read_len, pdMS_TO_TICKS(5));
|
||||
|
||||
if (bytes_read > 0)
|
||||
{
|
||||
size_t offset = 0;
|
||||
while (offset < bytes_read)
|
||||
{
|
||||
size_t chunk_size = (bytes_read - offset > CHUNK_SIZE) ? CHUNK_SIZE : (bytes_read - offset);
|
||||
|
||||
StatusMessage message = StatusMessage_init_zero;
|
||||
message.which_payload = StatusMessage_uart_data_tag;
|
||||
struct bytes_arg a = {.data = data_buf + offset, .len = chunk_size};
|
||||
message.payload.uart_data.data.funcs.encode = &encode_bytes_callback;
|
||||
message.payload.uart_data.data.arg = &a;
|
||||
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(pb_buffer, sizeof(pb_buffer));
|
||||
if (!pb_encode(&stream, StatusMessage_fields, &message))
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to encode uart data: %s", PB_GET_ERROR(&stream));
|
||||
offset += chunk_size;
|
||||
continue;
|
||||
}
|
||||
|
||||
struct ws_message msg;
|
||||
msg.type = WS_MSG_UART;
|
||||
msg.len = stream.bytes_written;
|
||||
msg.data = malloc(msg.len);
|
||||
|
||||
if (!msg.data)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for uart ws msg");
|
||||
offset += chunk_size;
|
||||
continue;
|
||||
}
|
||||
|
||||
memcpy(msg.data, pb_buffer, msg.len);
|
||||
|
||||
if (xQueueSend(ws_queue, &msg, pdMS_TO_TICKS(10)) != pdPASS)
|
||||
{
|
||||
ESP_LOGW(TAG, "ws sender queue full, dropping %zu bytes", chunk_size);
|
||||
free(msg.data);
|
||||
}
|
||||
|
||||
offset += chunk_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static void uart_event_task(void* arg)
|
||||
{
|
||||
uart_event_t event;
|
||||
while (1)
|
||||
{
|
||||
if (xQueueReceive(uart_event_queue, &event, portMAX_DELAY))
|
||||
{
|
||||
switch (event.type)
|
||||
{
|
||||
case UART_FIFO_OVF:
|
||||
ESP_LOGW(TAG, "UART HW FIFO Overflow");
|
||||
uart_flush_input(UART_NUM);
|
||||
xQueueReset(uart_event_queue);
|
||||
break;
|
||||
case UART_BUFFER_FULL:
|
||||
ESP_LOGW(TAG, "UART ring buffer full");
|
||||
uart_flush_input(UART_NUM);
|
||||
xQueueReset(uart_event_queue);
|
||||
break;
|
||||
case UART_DATA:
|
||||
// Muting this event because it is too noisy
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static esp_err_t ws_handler(httpd_req_t* req)
|
||||
{
|
||||
if (req->method == HTTP_GET)
|
||||
{
|
||||
ESP_LOGI(TAG, "WebSocket GET request received for URI: %s", req->uri);
|
||||
|
||||
char* query_str = NULL;
|
||||
size_t query_len = httpd_req_get_url_query_len(req) + 1;
|
||||
if (query_len > 1)
|
||||
{
|
||||
query_str = malloc(query_len);
|
||||
if (query_str == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for query string");
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Internal Server Error");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (httpd_req_get_url_query_str(req, query_str, query_len) != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to get query string from URI: %s", req->uri);
|
||||
free(query_str);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Internal Server Error");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ESP_LOGI(TAG, "Extracted query string: %s", query_str);
|
||||
}
|
||||
|
||||
char token_str[TOKEN_LENGTH];
|
||||
esp_err_t err = ESP_FAIL; // Default to fail
|
||||
|
||||
if (query_str)
|
||||
{
|
||||
err = httpd_query_key_value(query_str, "token", token_str, sizeof(token_str));
|
||||
free(query_str); // Free allocated query string
|
||||
}
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
ESP_LOGI(TAG, "Token extracted from query string, value: %s", token_str);
|
||||
if (!auth_validate_token(token_str))
|
||||
{
|
||||
ESP_LOGW(TAG, "WebSocket connection attempt with invalid token for URI: %s", req->uri);
|
||||
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Invalid or expired token");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ESP_LOGD(TAG, "WebSocket token validated for URI: %s", req->uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW(TAG, "Failed to extract token from query string or query string not found, error: %s",
|
||||
esp_err_to_name(err));
|
||||
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Authorization token required");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// No client is connected. Accept the new one.
|
||||
int new_fd = httpd_req_to_sockfd(req);
|
||||
ESP_LOGI(TAG, "Accepting new websocket connection: %d", new_fd);
|
||||
client_fd = new_fd;
|
||||
xSemaphoreGive(client_fd_mutex);
|
||||
|
||||
// Reset queue and flush UART buffer for the new session
|
||||
xQueueReset(ws_queue);
|
||||
uart_flush_input(UART_NUM);
|
||||
ESP_LOGI(TAG, "Handshake done, the new connection was opened");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
httpd_ws_frame_t ws_pkt = {0};
|
||||
uint8_t buf[BUF_SIZE];
|
||||
ws_pkt.payload = buf;
|
||||
ws_pkt.type = HTTPD_WS_TYPE_BINARY;
|
||||
|
||||
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, BUF_SIZE);
|
||||
if (ret != ESP_OK) {
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "httpd_ws_recv_frame failed with error: %s", esp_err_to_name(ret));
|
||||
xSemaphoreTake(client_fd_mutex, portMAX_DELAY);
|
||||
if (httpd_req_to_sockfd(req) == client_fd) {
|
||||
client_fd = -1;
|
||||
}
|
||||
xSemaphoreGive(client_fd_mutex);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uart_write_bytes(UART_NUM, (const char *)ws_pkt.payload, ws_pkt.len);
|
||||
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && ws_pkt.len == strlen("ping") &&
|
||||
strncmp((const char*)ws_pkt.payload, "ping", ws_pkt.len) == 0)
|
||||
{
|
||||
ESP_LOGD(TAG, "Received application-level ping from client, sending pong.");
|
||||
httpd_ws_frame_t pong_pkt = {
|
||||
.payload = (uint8_t*)"pong", .len = strlen("pong"), .type = HTTPD_WS_TYPE_TEXT, .final = true};
|
||||
return httpd_ws_send_frame(req, &pong_pkt);
|
||||
}
|
||||
else if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE)
|
||||
{
|
||||
ESP_LOGI(TAG, "Client sent close frame, closing connection.");
|
||||
return ESP_OK;
|
||||
}
|
||||
else if (ws_pkt.type == HTTPD_WS_TYPE_PING)
|
||||
{
|
||||
ESP_LOGD(TAG, "Received WebSocket PING control frame (handled by httpd).");
|
||||
return ESP_OK;
|
||||
}
|
||||
else if (ws_pkt.type == HTTPD_WS_TYPE_PONG)
|
||||
{
|
||||
ESP_LOGD(TAG, "Received WebSocket PONG control frame.");
|
||||
return ESP_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
uart_write_bytes(UART_NUM, (const char*)ws_pkt.payload, ws_pkt.len);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -269,7 +307,6 @@ static esp_err_t ws_handler(httpd_req_t *req) {
|
||||
void register_ws_endpoint(httpd_handle_t server)
|
||||
{
|
||||
size_t baud_rate_len;
|
||||
|
||||
nconfig_get_str_len(UART_BAUD_RATE, &baud_rate_len);
|
||||
char buf[baud_rate_len];
|
||||
nconfig_read(UART_BAUD_RATE, buf, baud_rate_len);
|
||||
@@ -280,42 +317,40 @@ void register_ws_endpoint(httpd_handle_t server)
|
||||
.parity = UART_PARITY_DISABLE,
|
||||
.stop_bits = UART_STOP_BITS_1,
|
||||
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||
// .source_clk = UART_SCLK_APB,
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(uart_param_config(UART_NUM, &uart_config));
|
||||
ESP_ERROR_CHECK(uart_set_pin(UART_NUM, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
|
||||
ESP_ERROR_CHECK(uart_driver_install(UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 0, NULL, 0));
|
||||
ESP_ERROR_CHECK(uart_driver_install(UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 20, &uart_event_queue, 0));
|
||||
|
||||
httpd_uri_t ws = {
|
||||
.uri = "/ws",
|
||||
.method = HTTP_GET,
|
||||
.handler = ws_handler,
|
||||
.user_ctx = NULL,
|
||||
.is_websocket = true
|
||||
};
|
||||
httpd_uri_t ws = {.uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .user_ctx = NULL, .is_websocket = true};
|
||||
httpd_register_uri_handler(server, &ws);
|
||||
|
||||
client_fd_mutex = xSemaphoreCreateMutex();
|
||||
ws_queue = xQueueCreate(10, sizeof(struct ws_message)); // Combined queue
|
||||
ws_queue = xQueueCreate(10, sizeof(struct ws_message));
|
||||
|
||||
xTaskCreate(uart_polling_task, "uart_polling_task", 1024*4, NULL, 8, NULL);
|
||||
xTaskCreate(unified_ws_sender_task, "ws_sender_task", 1024*6, server, 9, NULL);
|
||||
xTaskCreate(uart_polling_task, "uart_polling_task", 1024 * 4, NULL, 8, NULL);
|
||||
xTaskCreate(unified_ws_sender_task, "ws_sender_task", 1024 * 6, server, 9, NULL);
|
||||
xTaskCreate(uart_event_task, "uart_event_task", 1024 * 2, NULL, 10, NULL);
|
||||
}
|
||||
|
||||
void push_data_to_ws(cJSON *data)
|
||||
void push_data_to_ws(const uint8_t* data, size_t len)
|
||||
{
|
||||
struct ws_message msg;
|
||||
msg.type = WS_MSG_STATUS;
|
||||
msg.content.status.data = data;
|
||||
msg.data = malloc(len);
|
||||
if (!msg.data)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for status ws msg");
|
||||
return;
|
||||
}
|
||||
memcpy(msg.data, data, len);
|
||||
msg.len = len;
|
||||
|
||||
if (xQueueSend(ws_queue, &msg, pdMS_TO_TICKS(10)) != pdPASS)
|
||||
{
|
||||
ESP_LOGW(TAG, "WS queue full, dropping status message");
|
||||
cJSON_Delete(data);
|
||||
free(msg.data);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t change_baud_rate(int baud_rate)
|
||||
{
|
||||
return uart_set_baudrate(UART_NUM, baud_rate);
|
||||
}
|
||||
esp_err_t change_baud_rate(int baud_rate) { return uart_set_baudrate(UART_NUM, baud_rate); }
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// Created by shinys on 25. 8. 5.
|
||||
//
|
||||
|
||||
#include <system.h>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
static const char *TAG = "odroid";
|
||||
int t = 0;
|
||||
|
||||
TaskHandle_t reboot_handle = NULL;
|
||||
|
||||
static void reboot_task(void *arg)
|
||||
{
|
||||
while (t > 0)
|
||||
{
|
||||
ESP_LOGW(TAG, "ESP will reboot in [%d] sec..., If you want stop reboot, use command \"reboot -s\"", t);
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
--t;
|
||||
}
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
void start_reboot_timer(int sec)
|
||||
{
|
||||
|
||||
if (reboot_handle != NULL)
|
||||
{
|
||||
ESP_LOGW(TAG, "The reboot timer is already running.");
|
||||
return;
|
||||
}
|
||||
t = sec;
|
||||
xTaskCreate(reboot_task, "reboot_task", 2048, NULL, 8, &reboot_handle);
|
||||
}
|
||||
|
||||
void stop_reboot_timer()
|
||||
{
|
||||
if (reboot_handle == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
vTaskDelete(reboot_handle);
|
||||
}
|
||||
84
main/wifi/ap.c
Normal file
84
main/wifi/ap.c
Normal file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Created by shinys on 25. 9. 1.
|
||||
//
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "lwip/ip4_addr.h"
|
||||
#include "nconfig.h"
|
||||
#include "priv_wifi.h"
|
||||
#include "wifi.h"
|
||||
|
||||
static const char* TAG = "AP";
|
||||
|
||||
#define DEFAULT_AP_SSID "odroid-pm"
|
||||
#define DEFAULT_AP_PASS "powermate"
|
||||
#define AP_CHANNEL 1
|
||||
#define AP_MAX_CONN 4
|
||||
|
||||
/**
|
||||
* @brief Initializes and configures the AP mode.
|
||||
*/
|
||||
void wifi_init_ap(void)
|
||||
{
|
||||
// Get the network interface handle for the AP
|
||||
esp_netif_t* p_netif_ap = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
|
||||
|
||||
if (p_netif_ap)
|
||||
{
|
||||
ESP_LOGI(TAG, "Setting AP static IP to 192.168.4.1");
|
||||
esp_netif_dhcps_stop(p_netif_ap); // Stop DHCP server to apply new IP settings
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
IP4_ADDR(&ip_info.ip, 192, 168, 4, 1);
|
||||
IP4_ADDR(&ip_info.gw, 192, 168, 4, 1);
|
||||
IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0);
|
||||
esp_netif_set_ip_info(p_netif_ap, &ip_info);
|
||||
|
||||
esp_netif_dhcps_start(p_netif_ap); // Restart DHCP server
|
||||
}
|
||||
|
||||
// Configure Wi-Fi AP settings
|
||||
wifi_config_t wifi_config = {
|
||||
.ap =
|
||||
{
|
||||
.password = "",
|
||||
.channel = AP_CHANNEL,
|
||||
.max_connection = AP_MAX_CONN,
|
||||
.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
.pmf_cfg =
|
||||
{
|
||||
.required = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Read SSID and password from NVS (nconfig)
|
||||
size_t len;
|
||||
if (nconfig_get_str_len(AP_SSID, &len) == ESP_OK && len > 1)
|
||||
{
|
||||
nconfig_read(AP_SSID, (char*)wifi_config.ap.ssid, sizeof(wifi_config.ap.ssid));
|
||||
}
|
||||
else
|
||||
{
|
||||
strcpy((char*)wifi_config.ap.ssid, DEFAULT_AP_SSID);
|
||||
}
|
||||
|
||||
if (nconfig_get_str_len(AP_PASSWORD, &len) == ESP_OK && len > 1)
|
||||
{
|
||||
nconfig_read(AP_PASSWORD, (char*)wifi_config.ap.password, sizeof(wifi_config.ap.password));
|
||||
}
|
||||
|
||||
// If password is not set, use open authentication
|
||||
if (strlen((char*)wifi_config.ap.password) == 0)
|
||||
{
|
||||
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
|
||||
|
||||
ESP_LOGI(TAG, "wifi_init_ap finished. SSID: %s, Password: %s, Channel: %d", (char*)wifi_config.ap.ssid, "********",
|
||||
AP_CHANNEL);
|
||||
}
|
||||
13
main/wifi/priv_wifi.h
Normal file
13
main/wifi/priv_wifi.h
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// Created by shinys on 25. 9. 1..
|
||||
//
|
||||
|
||||
#ifndef ODROID_POWER_MATE_PRIV_WIFI_H
|
||||
#define ODROID_POWER_MATE_PRIV_WIFI_H
|
||||
|
||||
void wifi_init_sta(void);
|
||||
void wifi_init_ap(void);
|
||||
void initialize_sntp(void);
|
||||
void wifi_set_auto_reconnect(bool enable);
|
||||
|
||||
#endif // ODROID_POWER_MATE_PRIV_WIFI_H
|
||||
285
main/wifi/sta.c
Normal file
285
main/wifi/sta.c
Normal file
@@ -0,0 +1,285 @@
|
||||
//
|
||||
// Created by shinys on 25. 9. 1.
|
||||
//
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "lwip/inet.h"
|
||||
#include "nconfig.h"
|
||||
#include "priv_wifi.h"
|
||||
#include "wifi.h"
|
||||
|
||||
static const char* TAG = "STA";
|
||||
|
||||
/**
|
||||
* @brief Initializes and configures the STA mode.
|
||||
*/
|
||||
void wifi_init_sta(void)
|
||||
{
|
||||
wifi_config_t wifi_config = {0};
|
||||
|
||||
// Read SSID and password from NVS (nconfig)
|
||||
size_t len;
|
||||
if (nconfig_get_str_len(WIFI_SSID, &len) == ESP_OK && len > 1)
|
||||
{
|
||||
nconfig_read(WIFI_SSID, (char*)wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid));
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW(TAG, "STA SSID not configured in NVS.");
|
||||
}
|
||||
|
||||
if (nconfig_get_str_len(WIFI_PASSWORD, &len) == ESP_OK && len > 1)
|
||||
{
|
||||
nconfig_read(WIFI_PASSWORD, (char*)wifi_config.sta.password, sizeof(wifi_config.sta.password));
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW(TAG, "STA Password not configured in NVS.");
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
|
||||
// Check if we should use a static IP
|
||||
char netif_type[16] = {0};
|
||||
if (nconfig_get_str_len(NETIF_TYPE, &len) == ESP_OK && len > 1)
|
||||
{
|
||||
nconfig_read(NETIF_TYPE, netif_type, sizeof(netif_type));
|
||||
if (strcmp(netif_type, "static") == 0)
|
||||
{
|
||||
ESP_LOGI(TAG, "Using static IP configuration for STA.");
|
||||
char ip[16], gw[16], netmask[16], dns1[16], dns2[16];
|
||||
nconfig_read(NETIF_IP, ip, sizeof(ip));
|
||||
nconfig_read(NETIF_GATEWAY, gw, sizeof(gw));
|
||||
nconfig_read(NETIF_SUBNET, netmask, sizeof(netmask));
|
||||
nconfig_read(NETIF_DNS1, dns1, sizeof(dns1));
|
||||
nconfig_read(NETIF_DNS2, dns2, sizeof(dns2));
|
||||
wifi_use_static(ip, gw, netmask, dns1, dns2);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "wifi_init_sta finished.");
|
||||
}
|
||||
|
||||
esp_err_t wifi_connect(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Connecting to AP...");
|
||||
return esp_wifi_connect();
|
||||
}
|
||||
|
||||
esp_err_t wifi_disconnect(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Disconnecting from AP...");
|
||||
return esp_wifi_disconnect();
|
||||
}
|
||||
|
||||
void wifi_scan_aps(wifi_ap_record_t** ap_records, uint16_t* count)
|
||||
{
|
||||
ESP_LOGI(TAG, "Scanning for APs...");
|
||||
*count = 0;
|
||||
*ap_records = NULL;
|
||||
|
||||
wifi_set_auto_reconnect(false);
|
||||
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK)
|
||||
{
|
||||
esp_wifi_disconnect();
|
||||
}
|
||||
|
||||
// Start scan, this is a blocking call
|
||||
if (esp_wifi_scan_start(NULL, true) == ESP_OK)
|
||||
{
|
||||
esp_wifi_scan_get_ap_num(count);
|
||||
ESP_LOGI(TAG, "Found %d APs", *count);
|
||||
if (*count > 0)
|
||||
{
|
||||
*ap_records = (wifi_ap_record_t*)malloc(sizeof(wifi_ap_record_t) * (*count));
|
||||
if (*ap_records != NULL)
|
||||
{
|
||||
esp_wifi_scan_get_ap_records(count, *ap_records);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for AP records");
|
||||
*count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wifi_set_auto_reconnect(true);
|
||||
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK)
|
||||
{
|
||||
if (!nconfig_value_is_not_set(WIFI_SSID))
|
||||
{
|
||||
wifi_connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t wifi_get_current_ap_info(wifi_ap_record_t* ap_info)
|
||||
{
|
||||
if (ap_info == NULL)
|
||||
{
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
// This function retrieves the AP record to which the STA is currently connected.
|
||||
esp_err_t err = esp_wifi_sta_get_ap_info(ap_info);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to get connected AP info: %s", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t wifi_get_current_ip_info(esp_netif_ip_info_t* ip_info)
|
||||
{
|
||||
if (ip_info == NULL)
|
||||
{
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (netif == NULL)
|
||||
{
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return esp_netif_get_ip_info(netif, ip_info);
|
||||
}
|
||||
|
||||
esp_err_t wifi_get_dns_info(esp_netif_dns_type_t type, esp_netif_dns_info_t* dns_info)
|
||||
{
|
||||
if (dns_info == NULL)
|
||||
{
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (netif == NULL)
|
||||
{
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return esp_netif_get_dns_info(netif, type, dns_info);
|
||||
}
|
||||
|
||||
esp_err_t wifi_use_dhcp(void)
|
||||
{
|
||||
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (netif == NULL)
|
||||
{
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ESP_LOGI(TAG, "Setting STA to use DHCP");
|
||||
esp_err_t err = esp_netif_dhcpc_start(netif);
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
nconfig_write(NETIF_TYPE, "dhcp");
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t wifi_use_static(const char* ip, const char* gw, const char* netmask, const char* dns1, const char* dns2)
|
||||
{
|
||||
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (netif == NULL)
|
||||
{
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Setting STA to use static IP");
|
||||
esp_err_t err = esp_netif_dhcpc_stop(netif);
|
||||
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to stop DHCP client: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
ip_info.ip.addr = ipaddr_addr(ip);
|
||||
ip_info.gw.addr = ipaddr_addr(gw);
|
||||
ip_info.netmask.addr = ipaddr_addr(netmask);
|
||||
|
||||
err = esp_netif_set_ip_info(netif, &ip_info);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set static IP: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_netif_dns_info_t dns_info;
|
||||
dns_info.ip.u_addr.ip4.addr = ipaddr_addr(dns1);
|
||||
dns_info.ip.type = IPADDR_TYPE_V4;
|
||||
err = esp_netif_set_dns_info(netif, ESP_NETIF_DNS_MAIN, &dns_info);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set main DNS: %s", esp_err_to_name(err));
|
||||
// continue anyway
|
||||
}
|
||||
|
||||
if (dns2 && strlen(dns2) > 0)
|
||||
{
|
||||
dns_info.ip.u_addr.ip4.addr = ipaddr_addr(dns2);
|
||||
err = esp_netif_set_dns_info(netif, ESP_NETIF_DNS_BACKUP, &dns_info);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set backup DNS: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
// Save settings to NVS
|
||||
nconfig_write(NETIF_TYPE, "static");
|
||||
nconfig_write(NETIF_IP, ip);
|
||||
nconfig_write(NETIF_GATEWAY, gw);
|
||||
nconfig_write(NETIF_SUBNET, netmask);
|
||||
nconfig_write(NETIF_DNS1, dns1);
|
||||
nconfig_write(NETIF_DNS2, dns2);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t wifi_sta_set_ap(const char* ssid, const char* password)
|
||||
{
|
||||
ESP_LOGI(TAG, "Setting new AP with SSID: %s", ssid);
|
||||
|
||||
// Save settings to NVS first
|
||||
esp_err_t err = nconfig_write(WIFI_SSID, ssid);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to save SSID to NVS: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = nconfig_write(WIFI_PASSWORD, password);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to save password to NVS: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// Now configure the wifi interface
|
||||
wifi_config_t wifi_config = {0};
|
||||
strncpy((char*)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid));
|
||||
strncpy((char*)wifi_config.sta.password, password, sizeof(wifi_config.sta.password));
|
||||
|
||||
err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set Wi-Fi config: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// Disconnect from any current AP and connect to the new one
|
||||
ESP_LOGI(TAG, "Disconnecting from current AP if connected.");
|
||||
esp_wifi_disconnect();
|
||||
|
||||
ESP_LOGI(TAG, "Connecting to new AP...");
|
||||
err = esp_wifi_connect();
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to start connection to new AP: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
626
main/wifi/wifi.c
626
main/wifi/wifi.c
@@ -1,550 +1,168 @@
|
||||
//
|
||||
// Created by shinys on 25. 7. 10.
|
||||
// Created by shinys on 25. 9. 1.
|
||||
//
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "nconfig.h"
|
||||
#include "priv_wifi.h"
|
||||
|
||||
#include "wifi.h"
|
||||
|
||||
#include "nconfig.h"
|
||||
#include "indicator.h"
|
||||
static bool s_auto_reconnect = true;
|
||||
|
||||
#include <string.h>
|
||||
#include <system.h>
|
||||
#include <lwip/sockets.h>
|
||||
#include <time.h>
|
||||
static const char* TAG = "WIFI";
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "esp_event.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_wifi_default.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_netif_sntp.h"
|
||||
#include "rom/ets_sys.h"
|
||||
|
||||
static const char *TAG = "odroid";
|
||||
#define MAX_RETRY 10
|
||||
#define MAX_SCAN 20
|
||||
void wifi_set_auto_reconnect(bool enable) { s_auto_reconnect = enable; }
|
||||
|
||||
|
||||
const char* auth_mode_str(wifi_auth_mode_t mode)
|
||||
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
|
||||
{
|
||||
switch (mode)
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED)
|
||||
{
|
||||
case WIFI_AUTH_OPEN:
|
||||
return "OPEN";
|
||||
case WIFI_AUTH_WEP:
|
||||
return "WEP";
|
||||
case WIFI_AUTH_WPA_PSK:
|
||||
return "WPA_PSK";
|
||||
case WIFI_AUTH_WPA2_PSK:
|
||||
return "WPA2_PSK";
|
||||
case WIFI_AUTH_WPA_WPA2_PSK:
|
||||
return "WPA_WPA2_PSK";
|
||||
case WIFI_AUTH_ENTERPRISE:
|
||||
return "ENTERPRISE";
|
||||
case WIFI_AUTH_WPA3_PSK:
|
||||
return "WPA3_PSK";
|
||||
case WIFI_AUTH_WPA2_WPA3_PSK:
|
||||
return "WPA2_WPA3_PSK";
|
||||
case WIFI_AUTH_WAPI_PSK:
|
||||
return "WAPI_PSK";
|
||||
case WIFI_AUTH_OWE:
|
||||
return "OWE";
|
||||
case WIFI_AUTH_WPA3_ENT_192:
|
||||
return "WPA3_ENT_192";
|
||||
case WIFI_AUTH_WPA3_EXT_PSK:
|
||||
return "WPA3_EXT_PSK";
|
||||
case WIFI_AUTH_WPA3_EXT_PSK_MIXED_MODE:
|
||||
return "WPA3_EXT_PSK_MIXED_MODE";
|
||||
case WIFI_AUTH_DPP:
|
||||
return "DPP";
|
||||
case WIFI_AUTH_WPA3_ENTERPRISE:
|
||||
return "WPA3_ENTERPRISE";
|
||||
case WIFI_AUTH_WPA2_WPA3_ENTERPRISE:
|
||||
return "WPA2_WPA3_ENTERPRISE";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*)event_data;
|
||||
ESP_LOGI(TAG, "Station " MACSTR " joined, AID=%d", MAC2STR(event->mac), event->aid);
|
||||
}
|
||||
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED)
|
||||
{
|
||||
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*)event_data;
|
||||
ESP_LOGI(TAG, "Station " MACSTR " left, AID=%d", MAC2STR(event->mac), event->aid);
|
||||
}
|
||||
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
|
||||
{
|
||||
ESP_LOGI(TAG, "Station mode started");
|
||||
// Only try to connect if SSID is configured
|
||||
if (!nconfig_value_is_not_set(WIFI_SSID))
|
||||
{
|
||||
esp_wifi_connect();
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGI(TAG, "STA SSID not configured, not connecting.");
|
||||
}
|
||||
}
|
||||
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
|
||||
{
|
||||
led_set(LED_BLU, BLINK_TRIPLE);
|
||||
wifi_event_sta_disconnected_t* event = (wifi_event_sta_disconnected_t*)event_data;
|
||||
ESP_LOGW(TAG, "Disconnected from AP, reason: %s", wifi_reason_str(event->reason));
|
||||
|
||||
if (event->reason != WIFI_REASON_ASSOC_LEAVE)
|
||||
{
|
||||
if (s_auto_reconnect && !nconfig_value_is_not_set(WIFI_SSID))
|
||||
{
|
||||
ESP_LOGI(TAG, "Connection lost, attempting to reconnect...");
|
||||
esp_wifi_connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
|
||||
{
|
||||
led_set(LED_BLU, BLINK_SOLID);
|
||||
ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
|
||||
ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip));
|
||||
sync_time();
|
||||
}
|
||||
}
|
||||
|
||||
static const char* wifi_reason_str(wifi_err_reason_t reason) {
|
||||
switch (reason) {
|
||||
case WIFI_REASON_UNSPECIFIED: return "UNSPECIFIED";
|
||||
case WIFI_REASON_AUTH_EXPIRE: return "AUTH_EXPIRE";
|
||||
case WIFI_REASON_AUTH_LEAVE: return "AUTH_LEAVE";
|
||||
case WIFI_REASON_ASSOC_EXPIRE: return "ASSOC_EXPIRE";
|
||||
case WIFI_REASON_ASSOC_TOOMANY: return "ASSOC_TOOMANY";
|
||||
case WIFI_REASON_NOT_AUTHED: return "NOT_AUTHED";
|
||||
case WIFI_REASON_NOT_ASSOCED: return "NOT_ASSOCED";
|
||||
case WIFI_REASON_ASSOC_LEAVE: return "ASSOC_LEAVE";
|
||||
case WIFI_REASON_ASSOC_NOT_AUTHED: return "ASSOC_NOT_AUTHED";
|
||||
case WIFI_REASON_DISASSOC_PWRCAP_BAD: return "DISASSOC_PWRCAP_BAD";
|
||||
case WIFI_REASON_DISASSOC_SUPCHAN_BAD: return "DISASSOC_SUPCHAN_BAD";
|
||||
case WIFI_REASON_IE_INVALID: return "IE_INVALID";
|
||||
case WIFI_REASON_MIC_FAILURE: return "MIC_FAILURE";
|
||||
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: return "4WAY_HANDSHAKE_TIMEOUT";
|
||||
case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT: return "GROUP_KEY_UPDATE_TIMEOUT";
|
||||
case WIFI_REASON_IE_IN_4WAY_DIFFERS: return "IE_IN_4WAY_DIFFERS";
|
||||
case WIFI_REASON_GROUP_CIPHER_INVALID: return "GROUP_CIPHER_INVALID";
|
||||
case WIFI_REASON_PAIRWISE_CIPHER_INVALID: return "PAIRWISE_CIPHER_INVALID";
|
||||
case WIFI_REASON_AKMP_INVALID: return "AKMP_INVALID";
|
||||
case WIFI_REASON_UNSUPP_RSN_IE_VERSION: return "UNSUPP_RSN_IE_VERSION";
|
||||
case WIFI_REASON_INVALID_RSN_IE_CAP: return "INVALID_RSN_IE_CAP";
|
||||
case WIFI_REASON_802_1X_AUTH_FAILED: return "802_1X_AUTH_FAILED";
|
||||
case WIFI_REASON_CIPHER_SUITE_REJECTED: return "CIPHER_SUITE_REJECTED";
|
||||
case WIFI_REASON_INVALID_PMKID: return "INVALID_PMKID";
|
||||
case WIFI_REASON_BEACON_TIMEOUT: return "BEACON_TIMEOUT";
|
||||
case WIFI_REASON_NO_AP_FOUND: return "NO_AP_FOUND";
|
||||
case WIFI_REASON_AUTH_FAIL: return "AUTH_FAIL";
|
||||
case WIFI_REASON_ASSOC_FAIL: return "ASSOC_FAIL";
|
||||
case WIFI_REASON_HANDSHAKE_TIMEOUT: return "HANDSHAKE_TIMEOUT";
|
||||
case WIFI_REASON_CONNECTION_FAIL: return "CONNECTION_FAIL";
|
||||
case WIFI_REASON_AP_TSF_RESET: return "AP_TSF_RESET";
|
||||
case WIFI_REASON_ROAMING: return "ROAMING";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static esp_netif_t *wifi_sta_netif = NULL;
|
||||
static esp_netif_t *wifi_ap_netif = NULL;
|
||||
|
||||
static int s_retry_num = 0;
|
||||
|
||||
static esp_err_t wifi_sta_do_disconnect(void);
|
||||
|
||||
static void sntp_sync_time_cb(struct timeval *tv)
|
||||
void wifi_init(void)
|
||||
{
|
||||
time_t now = 0;
|
||||
struct tm timeinfo = { 0 };
|
||||
time(&now);
|
||||
localtime_r(&now, &timeinfo);
|
||||
char strftime_buf[64];
|
||||
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
|
||||
ESP_LOGI(TAG, "Time synchronized: %s", strftime_buf);
|
||||
}
|
||||
|
||||
static void handler_on_wifi_disconnect(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
s_retry_num++;
|
||||
if (s_retry_num > MAX_RETRY) {
|
||||
ESP_LOGW(TAG, "WiFi Connect failed %d times, stop reconnect.", s_retry_num);
|
||||
/* let example_wifi_sta_do_connect() return */
|
||||
|
||||
wifi_sta_do_disconnect();
|
||||
start_reboot_timer(60);
|
||||
return;
|
||||
}
|
||||
wifi_event_sta_disconnected_t *disconn = event_data;
|
||||
if (disconn->reason == WIFI_REASON_ROAMING) {
|
||||
ESP_LOGD(TAG, "station roaming, do nothing");
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Wi-Fi disconnected, reason: (%s)", wifi_reason_str(disconn->reason));
|
||||
ESP_LOGI(TAG, "Trying to reconnect...");
|
||||
esp_err_t err = esp_wifi_connect();
|
||||
if (err == ESP_ERR_WIFI_NOT_STARTED) {
|
||||
return;
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
}
|
||||
|
||||
static void handler_on_wifi_connect(void *esp_netif, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
static void handler_on_sta_got_ip(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
stop_reboot_timer();
|
||||
s_retry_num = 0;
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||
if (strcmp("sta", esp_netif_get_desc(event->esp_netif)) != 0) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "Got IPv4 event: Interface \"%s\" address: " IPSTR, esp_netif_get_desc(event->esp_netif), IP2STR(&event->ip_info.ip));
|
||||
ESP_LOGI(TAG, "- IPv4 address: " IPSTR ",", IP2STR(&event->ip_info.ip));
|
||||
sync_time();
|
||||
led_set(LED_BLU, BLINK_SOLID);
|
||||
}
|
||||
|
||||
static void wifi_ap_event_handler(void* arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void* event_data)
|
||||
{
|
||||
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
|
||||
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
|
||||
ESP_LOGI(TAG, "station "MACSTR" join, AID=%d",
|
||||
MAC2STR(event->mac), event->aid);
|
||||
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
|
||||
ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d",
|
||||
MAC2STR(event->mac), event->aid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static esp_err_t set_hostname(esp_netif_t* esp_netif, const char *hostname)
|
||||
{
|
||||
if (esp_netif_set_hostname(esp_netif, hostname) != ESP_OK) return ESP_FAIL;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void wifi_start(wifi_mode_t mode)
|
||||
{
|
||||
size_t hostname_len;
|
||||
char type_buf[16];
|
||||
// Create network interfaces for both AP and STA.
|
||||
// This is done unconditionally to allow for dynamic mode switching.
|
||||
esp_netif_create_default_wifi_ap();
|
||||
esp_netif_create_default_wifi_sta();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
if (mode == WIFI_MODE_STA || mode == WIFI_MODE_APSTA) {
|
||||
esp_netif_inherent_config_t esp_netif_config = ESP_NETIF_INHERENT_DEFAULT_WIFI_STA();
|
||||
wifi_sta_netif = esp_netif_create_wifi(WIFI_IF_STA, &esp_netif_config);
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL));
|
||||
|
||||
if (nconfig_read(NETIF_TYPE, type_buf, sizeof(type_buf)) == ESP_OK && strcmp(type_buf, "static") == 0) {
|
||||
ESP_LOGI(TAG, "Using static IP configuration");
|
||||
char ip_buf[16], gw_buf[16], mask_buf[16], dns1_buf[16], dns2_buf[16];
|
||||
nconfig_read(NETIF_IP, ip_buf, sizeof(ip_buf));
|
||||
nconfig_read(NETIF_GATEWAY, gw_buf, sizeof(gw_buf));
|
||||
nconfig_read(NETIF_SUBNET, mask_buf, sizeof(mask_buf));
|
||||
const char* dns1 = (nconfig_read(NETIF_DNS1, dns1_buf, sizeof(dns1_buf)) == ESP_OK) ? dns1_buf : NULL;
|
||||
const char* dns2 = (nconfig_read(NETIF_DNS2, dns2_buf, sizeof(dns2_buf)) == ESP_OK) ? dns2_buf : NULL;
|
||||
if (dns1 == NULL)
|
||||
wifi_use_static(ip_buf, gw_buf, mask_buf, "8.8.8.8", "8.8.4.4");
|
||||
else
|
||||
wifi_use_static(ip_buf, gw_buf, mask_buf, dns1, dns2);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Using DHCP configuration");
|
||||
wifi_use_dhcp();
|
||||
}
|
||||
initialize_sntp();
|
||||
|
||||
nconfig_get_str_len(NETIF_HOSTNAME, &hostname_len);
|
||||
char buf[hostname_len];
|
||||
nconfig_read(NETIF_HOSTNAME, buf, sizeof(buf));
|
||||
set_hostname(wifi_sta_netif, buf);
|
||||
}
|
||||
char mode_str[10] = {0};
|
||||
wifi_mode_t mode = WIFI_MODE_APSTA;
|
||||
const char* started_mode_str = "APSTA";
|
||||
|
||||
if (mode == WIFI_MODE_AP || mode == WIFI_MODE_APSTA) {
|
||||
esp_netif_inherent_config_t esp_netif_config_ap = ESP_NETIF_INHERENT_DEFAULT_WIFI_AP();
|
||||
wifi_ap_netif = esp_netif_create_wifi(WIFI_IF_AP, &esp_netif_config_ap);
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &wifi_ap_event_handler, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &wifi_ap_event_handler, NULL));
|
||||
}
|
||||
|
||||
esp_wifi_set_default_wifi_sta_handlers();
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(mode));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
}
|
||||
|
||||
|
||||
static void wifi_stop(void)
|
||||
{
|
||||
esp_err_t err = esp_wifi_stop();
|
||||
if (err == ESP_ERR_WIFI_NOT_INIT) {
|
||||
return;
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
if (wifi_ap_netif) {
|
||||
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &wifi_ap_event_handler);
|
||||
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &wifi_ap_event_handler);
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_deinit());
|
||||
|
||||
if (wifi_sta_netif) {
|
||||
esp_netif_destroy(wifi_sta_netif);
|
||||
wifi_sta_netif = NULL;
|
||||
}
|
||||
if (wifi_ap_netif) {
|
||||
esp_netif_destroy(wifi_ap_netif);
|
||||
wifi_ap_netif = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static esp_err_t wifi_sta_do_connect(wifi_config_t wifi_config)
|
||||
{
|
||||
stop_reboot_timer();
|
||||
s_retry_num = 0;
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, handler_on_wifi_disconnect, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, handler_on_sta_got_ip, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, handler_on_wifi_connect, wifi_sta_netif));
|
||||
|
||||
|
||||
ESP_LOGI(TAG, "Connecting to %s...", (char*)wifi_config.sta.ssid);
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
esp_err_t ret = esp_wifi_connect();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "WiFi connect failed! ret:%x", ret);
|
||||
return ret;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t wifi_sta_do_disconnect(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &handler_on_wifi_disconnect));
|
||||
ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &handler_on_sta_got_ip));
|
||||
ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &handler_on_wifi_connect));
|
||||
led_set(LED_BLU, BLINK_DOUBLE);
|
||||
|
||||
return esp_wifi_disconnect();
|
||||
}
|
||||
|
||||
static void wifi_shutdown(void)
|
||||
{
|
||||
wifi_sta_do_disconnect();
|
||||
wifi_stop();
|
||||
}
|
||||
|
||||
static esp_err_t do_connect(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
char mode_buf[16] = {0};
|
||||
wifi_mode_t mode = WIFI_MODE_STA; // Default mode
|
||||
|
||||
if (nconfig_read(WIFI_MODE, mode_buf, sizeof(mode_buf)) == ESP_OK) {
|
||||
if (strcmp(mode_buf, "apsta") == 0) {
|
||||
mode = WIFI_MODE_APSTA;
|
||||
ESP_LOGI(TAG, "Starting in APSTA mode");
|
||||
} else { // "sta" or anything else defaults to STA
|
||||
if (nconfig_read(WIFI_MODE, mode_str, sizeof(mode_str)) == ESP_OK)
|
||||
{
|
||||
if (strcmp(mode_str, "sta") == 0)
|
||||
{
|
||||
mode = WIFI_MODE_STA;
|
||||
ESP_LOGI(TAG, "Starting in STA mode");
|
||||
started_mode_str = "STA";
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "WIFI_MODE not set, defaulting to STA mode");
|
||||
}
|
||||
|
||||
wifi_start(mode);
|
||||
|
||||
// Configure and connect STA interface if needed
|
||||
if (mode == WIFI_MODE_STA || mode == WIFI_MODE_APSTA) {
|
||||
wifi_config_t sta_config = {0};
|
||||
bool sta_creds_ok = false;
|
||||
if (nconfig_read(WIFI_SSID, (char*)sta_config.sta.ssid, 32) == ESP_OK && strlen((char*)sta_config.sta.ssid) > 0) {
|
||||
if (nconfig_read(WIFI_PASSWORD, (char*)sta_config.sta.password, 64) == ESP_OK) {
|
||||
sta_creds_ok = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sta_creds_ok) {
|
||||
err = wifi_sta_do_connect(sta_config);
|
||||
if (err != ESP_OK && mode == WIFI_MODE_STA) {
|
||||
// In STA-only mode, failure to connect is a fatal error
|
||||
return err;
|
||||
}
|
||||
} else if (mode == WIFI_MODE_STA) {
|
||||
// In STA-only mode, missing credentials is a fatal error
|
||||
ESP_LOGE(TAG, "Missing STA credentials in STA mode.");
|
||||
return ESP_FAIL;
|
||||
} else {
|
||||
// In APSTA mode, missing credentials is a warning
|
||||
ESP_LOGW(TAG, "Missing STA credentials in APSTA mode. STA will not connect.");
|
||||
else if (strcmp(mode_str, "apsta") != 0)
|
||||
{
|
||||
ESP_LOGW(TAG, "Invalid Wi-Fi mode in nconfig: '%s'. Defaulting to APSTA.", mode_str);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure AP interface if needed
|
||||
if (mode == WIFI_MODE_AP || mode == WIFI_MODE_APSTA) {
|
||||
char ap_ssid[32], ap_pass[64];
|
||||
wifi_config_t ap_config = {
|
||||
.ap = {
|
||||
.channel = 1,
|
||||
.max_connection = 4,
|
||||
.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
.ssid_hidden = 0,
|
||||
},
|
||||
};
|
||||
|
||||
if (nconfig_read(AP_SSID, ap_ssid, sizeof(ap_ssid)) == ESP_OK && strlen(ap_ssid) > 0) {
|
||||
strcpy((char*)ap_config.ap.ssid, ap_ssid);
|
||||
} else {
|
||||
strcpy((char*)ap_config.ap.ssid, "ODROID-REMOTE-AP");
|
||||
}
|
||||
|
||||
if (nconfig_read(AP_PASSWORD, ap_pass, sizeof(ap_pass)) == ESP_OK && strlen(ap_pass) >= 8) {
|
||||
strcpy((char*)ap_config.ap.password, ap_pass);
|
||||
} else {
|
||||
ap_config.ap.authmode = WIFI_AUTH_OPEN;
|
||||
memset(ap_config.ap.password, 0, sizeof(ap_config.ap.password));
|
||||
}
|
||||
ap_config.ap.ssid_len = strlen((char*)ap_config.ap.ssid);
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_config));
|
||||
ESP_LOGI(TAG, "AP configured, SSID: %s", ap_config.ap.ssid);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t wifi_connect(void)
|
||||
{
|
||||
led_set(LED_BLU, BLINK_DOUBLE);
|
||||
|
||||
static esp_sntp_config_t ntp_cfg = ESP_NETIF_SNTP_DEFAULT_CONFIG_MULTIPLE(3,
|
||||
ESP_SNTP_SERVER_LIST("time.windows.com", "pool.ntp.org", "216.239.35.0")); // google public ntp
|
||||
ntp_cfg.start = false;
|
||||
ntp_cfg.sync_cb = sntp_sync_time_cb;
|
||||
ntp_cfg.smooth_sync = true; // Sync immediately when started
|
||||
esp_netif_sntp_init(&ntp_cfg);
|
||||
|
||||
if (do_connect() != ESP_OK) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ESP_ERROR_CHECK(esp_register_shutdown_handler(&wifi_shutdown));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
esp_err_t wifi_disconnect(void)
|
||||
{
|
||||
wifi_shutdown();
|
||||
ESP_ERROR_CHECK(esp_unregister_shutdown_handler(&wifi_shutdown));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void wifi_scan_aps(wifi_ap_record_t **ap_records, uint16_t* count)
|
||||
{
|
||||
ESP_LOGI(TAG, "Starting WiFi scan...");
|
||||
|
||||
esp_err_t err = esp_wifi_scan_start(NULL, true);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_wifi_scan_start failed: %s", esp_err_to_name(err));
|
||||
*count = 0;
|
||||
*ap_records = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(count));
|
||||
ESP_LOGI(TAG, "Found %u access points", *count);
|
||||
|
||||
if (*count == 0)
|
||||
*ap_records = NULL;
|
||||
else
|
||||
*ap_records = calloc(*count, sizeof(wifi_ap_record_t));
|
||||
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(count, *ap_records));
|
||||
ESP_LOGI(TAG, "Scan done");
|
||||
}
|
||||
|
||||
esp_err_t wifi_get_current_ap_info(wifi_ap_record_t *ap_info)
|
||||
{
|
||||
esp_err_t ret = esp_wifi_sta_get_ap_info(ap_info);
|
||||
if (ret != ESP_OK) {
|
||||
// Clear ssid and set invalid rssi on error
|
||||
memset(ap_info->ssid, 0, sizeof(ap_info->ssid));
|
||||
ap_info->rssi = -127;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t wifi_get_current_ip_info(esp_netif_ip_info_t *ip_info)
|
||||
{
|
||||
return esp_netif_get_ip_info(wifi_sta_netif, ip_info);
|
||||
}
|
||||
|
||||
esp_err_t wifi_get_dns_info(esp_netif_dns_type_t type, esp_netif_dns_info_t *dns_info)
|
||||
{
|
||||
if (wifi_sta_netif) {
|
||||
return esp_netif_get_dns_info(wifi_sta_netif, type, dns_info);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t wifi_use_static(const char *ip, const char *gw, const char *netmask, const char *dns1, const char *dns2)
|
||||
{
|
||||
if (wifi_sta_netif == NULL) {
|
||||
return ESP_FAIL;
|
||||
{
|
||||
ESP_LOGW(TAG, "Failed to read Wi-Fi mode from nconfig. Defaulting to APSTA.");
|
||||
}
|
||||
|
||||
esp_err_t err = esp_netif_dhcpc_stop(wifi_sta_netif);
|
||||
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
|
||||
ESP_LOGE(TAG, "Failed to stop DHCP client: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(mode));
|
||||
|
||||
if (mode == WIFI_MODE_APSTA)
|
||||
{
|
||||
wifi_init_ap();
|
||||
wifi_init_sta();
|
||||
}
|
||||
else if (mode == WIFI_MODE_STA)
|
||||
{
|
||||
wifi_init_sta();
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
inet_pton(AF_INET, ip, &ip_info.ip);
|
||||
inet_pton(AF_INET, gw, &ip_info.gw);
|
||||
inet_pton(AF_INET, netmask, &ip_info.netmask);
|
||||
err = esp_netif_set_ip_info(wifi_sta_netif, &ip_info);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set static IP info: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
esp_netif_dns_info_t dns_info;
|
||||
if (dns1 && strlen(dns1) > 0) {
|
||||
inet_pton(AF_INET, dns1, &dns_info.ip.u_addr.ip4);
|
||||
esp_netif_set_dns_info(wifi_sta_netif, ESP_NETIF_DNS_MAIN, &dns_info);
|
||||
} else {
|
||||
esp_netif_set_dns_info(wifi_sta_netif, ESP_NETIF_DNS_MAIN, NULL);
|
||||
}
|
||||
|
||||
if (dns2 && strlen(dns2) > 0) {
|
||||
inet_pton(AF_INET, dns2, &dns_info.ip.u_addr.ip4);
|
||||
esp_netif_set_dns_info(wifi_sta_netif, ESP_NETIF_DNS_BACKUP, &dns_info);
|
||||
} else {
|
||||
esp_netif_set_dns_info(wifi_sta_netif, ESP_NETIF_DNS_BACKUP, NULL);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t wifi_use_dhcp(void)
|
||||
{
|
||||
if (wifi_sta_netif == NULL) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
esp_err_t err = esp_netif_dhcpc_start(wifi_sta_netif);
|
||||
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
|
||||
ESP_LOGE(TAG, "Failed to start DHCP client: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
led_set(LED_BLU, BLINK_TRIPLE);
|
||||
ESP_LOGI(TAG, "wifi_init_all finished. Started in %s mode.", started_mode_str);
|
||||
}
|
||||
|
||||
esp_err_t wifi_switch_mode(const char* mode)
|
||||
{
|
||||
if (strcmp(mode, "sta") != 0 && strcmp(mode, "apsta") != 0) {
|
||||
ESP_LOGE(TAG, "Invalid mode specified: %s. Use 'sta' or 'apsta'.", mode);
|
||||
ESP_LOGI(TAG, "Switching Wi-Fi mode to %s", mode);
|
||||
|
||||
wifi_mode_t new_mode;
|
||||
if (strcmp(mode, "sta") == 0)
|
||||
{
|
||||
new_mode = WIFI_MODE_STA;
|
||||
}
|
||||
else if (strcmp(mode, "apsta") == 0)
|
||||
{
|
||||
new_mode = WIFI_MODE_APSTA;
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Unsupported mode: %s", mode);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
char current_mode_buf[16] = {0};
|
||||
if (nconfig_read(WIFI_MODE, current_mode_buf, sizeof(current_mode_buf)) == ESP_OK) {
|
||||
if (strcmp(current_mode_buf, mode) == 0) {
|
||||
ESP_LOGI(TAG, "Already in %s mode.", mode);
|
||||
return ESP_OK;
|
||||
}
|
||||
nconfig_write(WIFI_MODE, mode);
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_stop());
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(new_mode));
|
||||
|
||||
if (new_mode == WIFI_MODE_APSTA)
|
||||
{
|
||||
wifi_init_ap();
|
||||
wifi_init_sta();
|
||||
}
|
||||
else if (new_mode == WIFI_MODE_STA)
|
||||
{
|
||||
wifi_init_sta();
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Switching Wi-Fi mode to %s.", mode);
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
esp_err_t err = nconfig_write(WIFI_MODE, mode);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to save new Wi-Fi mode to NVS");
|
||||
return err;
|
||||
}
|
||||
ESP_LOGI(TAG, "Wi-Fi mode switched to %s", mode);
|
||||
|
||||
wifi_disconnect();
|
||||
err = wifi_connect();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to connect in new mode %s", mode);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Successfully switched to %s mode.", mode);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void sync_time()
|
||||
{
|
||||
esp_netif_sntp_start();
|
||||
ESP_LOGI(TAG, "SNTP service started, waiting for time synchronization...");
|
||||
}
|
||||
160
main/wifi/wifi_helper.c
Normal file
160
main/wifi/wifi_helper.c
Normal file
@@ -0,0 +1,160 @@
|
||||
//
|
||||
// Created by shinys on 25. 9. 1.
|
||||
//
|
||||
|
||||
#include <time.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_sntp.h"
|
||||
#include "priv_wifi.h"
|
||||
#include "wifi.h"
|
||||
|
||||
static const char* TAG = "WIFI_HELPER";
|
||||
|
||||
const char* auth_mode_str(wifi_auth_mode_t mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case WIFI_AUTH_OPEN:
|
||||
return "OPEN";
|
||||
case WIFI_AUTH_WEP:
|
||||
return "WEP";
|
||||
case WIFI_AUTH_WPA_PSK:
|
||||
return "WPA_PSK";
|
||||
case WIFI_AUTH_WPA2_PSK:
|
||||
return "WPA2_PSK";
|
||||
case WIFI_AUTH_WPA_WPA2_PSK:
|
||||
return "WPA_WPA2_PSK";
|
||||
case WIFI_AUTH_ENTERPRISE:
|
||||
return "ENTERPRISE";
|
||||
case WIFI_AUTH_WPA3_PSK:
|
||||
return "WPA3_PSK";
|
||||
case WIFI_AUTH_WPA2_WPA3_PSK:
|
||||
return "WPA2_WPA3_PSK";
|
||||
case WIFI_AUTH_WAPI_PSK:
|
||||
return "WAPI_PSK";
|
||||
case WIFI_AUTH_OWE:
|
||||
return "OWE";
|
||||
case WIFI_AUTH_WPA3_ENT_192:
|
||||
return "WPA3_ENT_192";
|
||||
case WIFI_AUTH_WPA3_EXT_PSK:
|
||||
return "WPA3_EXT_PSK";
|
||||
case WIFI_AUTH_WPA3_EXT_PSK_MIXED_MODE:
|
||||
return "WPA3_EXT_PSK_MIXED_MODE";
|
||||
case WIFI_AUTH_DPP:
|
||||
return "DPP";
|
||||
case WIFI_AUTH_WPA3_ENTERPRISE:
|
||||
return "WPA3_ENTERPRISE";
|
||||
case WIFI_AUTH_WPA2_WPA3_ENTERPRISE:
|
||||
return "WPA2_WPA3_ENTERPRISE";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const char* wifi_reason_str(wifi_err_reason_t reason)
|
||||
{
|
||||
switch (reason)
|
||||
{
|
||||
case WIFI_REASON_UNSPECIFIED:
|
||||
return "UNSPECIFIED";
|
||||
case WIFI_REASON_AUTH_EXPIRE:
|
||||
return "AUTH_EXPIRE";
|
||||
case WIFI_REASON_AUTH_LEAVE:
|
||||
return "AUTH_LEAVE";
|
||||
case WIFI_REASON_ASSOC_EXPIRE:
|
||||
return "ASSOC_EXPIRE";
|
||||
case WIFI_REASON_ASSOC_TOOMANY:
|
||||
return "ASSOC_TOOMANY";
|
||||
case WIFI_REASON_NOT_AUTHED:
|
||||
return "NOT_AUTHED";
|
||||
case WIFI_REASON_NOT_ASSOCED:
|
||||
return "NOT_ASSOCED";
|
||||
case WIFI_REASON_ASSOC_LEAVE:
|
||||
return "ASSOC_LEAVE";
|
||||
case WIFI_REASON_ASSOC_NOT_AUTHED:
|
||||
return "ASSOC_NOT_AUTHED";
|
||||
case WIFI_REASON_DISASSOC_PWRCAP_BAD:
|
||||
return "DISASSOC_PWRCAP_BAD";
|
||||
case WIFI_REASON_DISASSOC_SUPCHAN_BAD:
|
||||
return "DISASSOC_SUPCHAN_BAD";
|
||||
case WIFI_REASON_IE_INVALID:
|
||||
return "IE_INVALID";
|
||||
case WIFI_REASON_MIC_FAILURE:
|
||||
return "MIC_FAILURE";
|
||||
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:
|
||||
return "4WAY_HANDSHAKE_TIMEOUT";
|
||||
case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT:
|
||||
return "GROUP_KEY_UPDATE_TIMEOUT";
|
||||
case WIFI_REASON_IE_IN_4WAY_DIFFERS:
|
||||
return "IE_IN_4WAY_DIFFERS";
|
||||
case WIFI_REASON_GROUP_CIPHER_INVALID:
|
||||
return "GROUP_CIPHER_INVALID";
|
||||
case WIFI_REASON_PAIRWISE_CIPHER_INVALID:
|
||||
return "PAIRWISE_CIPHER_INVALID";
|
||||
case WIFI_REASON_AKMP_INVALID:
|
||||
return "AKMP_INVALID";
|
||||
case WIFI_REASON_UNSUPP_RSN_IE_VERSION:
|
||||
return "UNSUPP_RSN_IE_VERSION";
|
||||
case WIFI_REASON_INVALID_RSN_IE_CAP:
|
||||
return "INVALID_RSN_IE_CAP";
|
||||
case WIFI_REASON_802_1X_AUTH_FAILED:
|
||||
return "802_1X_AUTH_FAILED";
|
||||
case WIFI_REASON_CIPHER_SUITE_REJECTED:
|
||||
return "CIPHER_SUITE_REJECTED";
|
||||
case WIFI_REASON_INVALID_PMKID:
|
||||
return "INVALID_PMKID";
|
||||
case WIFI_REASON_BEACON_TIMEOUT:
|
||||
return "BEACON_TIMEOUT";
|
||||
case WIFI_REASON_NO_AP_FOUND:
|
||||
return "NO_AP_FOUND";
|
||||
case WIFI_REASON_AUTH_FAIL:
|
||||
return "AUTH_FAIL";
|
||||
case WIFI_REASON_ASSOC_FAIL:
|
||||
return "ASSOC_FAIL";
|
||||
case WIFI_REASON_HANDSHAKE_TIMEOUT:
|
||||
return "HANDSHAKE_TIMEOUT";
|
||||
case WIFI_REASON_CONNECTION_FAIL:
|
||||
return "CONNECTION_FAIL";
|
||||
case WIFI_REASON_AP_TSF_RESET:
|
||||
return "AP_TSF_RESET";
|
||||
case WIFI_REASON_ROAMING:
|
||||
return "ROAMING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
// Callback function for time synchronization
|
||||
void time_sync_notification_cb(struct timeval* tv)
|
||||
{
|
||||
ESP_LOGI(TAG, "Time synchronized");
|
||||
// Set timezone to UTC
|
||||
setenv("TZ", "UTC", 1);
|
||||
tzset();
|
||||
|
||||
char strftime_buf[64];
|
||||
time_t now;
|
||||
struct tm timeinfo;
|
||||
time(&now);
|
||||
localtime_r(&now, &timeinfo);
|
||||
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
|
||||
ESP_LOGI(TAG, "The current date/time in UTC is: %s", strftime_buf);
|
||||
}
|
||||
|
||||
void initialize_sntp(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing SNTP service");
|
||||
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
||||
esp_sntp_setservername(0, "pool.ntp.org");
|
||||
sntp_set_time_sync_notification_cb(time_sync_notification_cb);
|
||||
}
|
||||
|
||||
void sync_time()
|
||||
{
|
||||
if (esp_sntp_enabled())
|
||||
{
|
||||
esp_sntp_stop();
|
||||
}
|
||||
ESP_LOGI(TAG, "Starting SNTP synchronization");
|
||||
esp_sntp_init();
|
||||
}
|
||||
138
page/index.html
138
page/index.html
@@ -2,13 +2,39 @@
|
||||
<html lang="en" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ODROID Remote</title>
|
||||
<title>ODROID PowerMate</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main class="container">
|
||||
<div id="login-container" class="d-flex flex-column justify-content-center align-items-center vh-100" style="display: none;">
|
||||
<div class="card p-4 shadow-lg" style="width: 100%; max-width: 400px;">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-center mb-4">Login to ODROID Power Mate</h2>
|
||||
<div id="login-alert" class="alert alert-danger d-none" role="alert"></div>
|
||||
<form id="login-form">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="form-check form-switch d-flex justify-content-center mt-4">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="theme-toggle-login">
|
||||
<label class="form-check-label ms-2" for="theme-toggle-login"><i id="theme-icon-login" class="bi bi-moon-stars-fill"></i></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="container" style="display: none;">
|
||||
<header class="d-flex justify-content-between align-items-center mb-3 main-header">
|
||||
<div class="order-md-1" style="flex: 1;">
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -27,7 +53,10 @@
|
||||
<span id="power-display" class="text-primary">--.-- W</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-primary text-center order-md-2 mx-auto">ODROID Power Mate</h1>
|
||||
<div class="text-center order-md-2 mx-auto">
|
||||
<h1 class="text-primary mb-0">ODROID Power Mate</h1>
|
||||
<small class="text-muted" id="version-info"></small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-end order-md-3 header-controls" style="flex: 1;">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="theme-toggle">
|
||||
@@ -37,6 +66,9 @@
|
||||
data-bs-target="#settingsModal">
|
||||
<i class="bi bi-gear"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary ms-3" id="logout-button">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -71,7 +103,7 @@
|
||||
<div class="mt-4">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center control-list-item">
|
||||
Main Power (12V)
|
||||
Main Power
|
||||
<div class="control-wrapper">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="main-power-toggle">
|
||||
@@ -79,7 +111,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center control-list-item">
|
||||
USB Power (5V)
|
||||
USB Power
|
||||
<div class="control-wrapper">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="usb-power-toggle">
|
||||
@@ -109,9 +141,11 @@
|
||||
<div class="card border-top-0 rounded-0 rounded-bottom">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<a href="/datalog.csv" class="btn btn-primary" download="datalog.csv"><i class="bi bi-download me-1"></i> Download CSV</a>
|
||||
<button id="record-button" class="btn btn-success me-2"><i class="bi bi-record-circle me-1"></i>Record</button>
|
||||
<button id="stop-button" class="btn btn-danger me-2" style="display: none;"><i class="bi bi-stop-circle me-1"></i>Stop</button>
|
||||
<button id="download-csv-button" class="btn btn-primary" style="display: none;"><i class="bi bi-download me-1"></i>Download CSV</button>
|
||||
</div>
|
||||
<h5 class="card-title text-center mb-3">Power Input</h5>
|
||||
<h5 class="card-title text-center mb-3">Power Metrics</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3 mb-md-0">
|
||||
<canvas id="powerChart" class="chart-canvas"></canvas>
|
||||
@@ -130,8 +164,8 @@
|
||||
</main>
|
||||
|
||||
<footer class="bg-body-tertiary text-center p-3">
|
||||
<a href="https://www.hardkernel.com/" target="_blank" class="link-secondary">Hardkernel</a> |
|
||||
<a href="https://wiki.odroid.com/start" target="_blank" class="link-secondary">Wiki</a>
|
||||
<a href="https://www.hardkernel.com/" target="_blank" class="link-secondary text-decoration-none">Hardkernel</a> |
|
||||
<a href="https://wiki.odroid.com/start" target="_blank" class="link-secondary text-decoration-none">Wiki</a>
|
||||
</footer>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
@@ -159,6 +193,16 @@
|
||||
data-bs-target="#ap-mode-settings-pane" type="button" role="tab">AP Mode
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-target="#current-limit-settings-pane" data-bs-toggle="tab"
|
||||
id="current-limit-settings-tab" role="tab" type="button">Current Limit
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-target="#user-settings-pane" data-bs-toggle="tab"
|
||||
id="user-settings-tab" role="tab" type="button">User Settings
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="device-settings-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#device-settings-pane" type="button" role="tab">Device
|
||||
@@ -251,7 +295,7 @@
|
||||
<div id="ap-mode-config" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="ap-ssid" class="form-label">AP SSID</label>
|
||||
<input type="text" class="form-control" id="ap-ssid" placeholder="ODROID-Remote-AP">
|
||||
<input type="text" class="form-control" id="ap-ssid" placeholder="powermate">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ap-password" class="form-label">AP Password</label>
|
||||
@@ -260,11 +304,63 @@
|
||||
</div>
|
||||
<div class="d-flex justify-content-end pt-3 border-top mt-3">
|
||||
<button type="button" class="btn btn-primary me-2" id="ap-mode-apply-button">Apply</button>
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="current-limit-settings-pane" role="tabpanel">
|
||||
<div class="alert alert-info mt-3" role="alert">
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
Set a value of <strong>0.0</strong> to disable a specific current limit. The value will be
|
||||
rounded to one decimal place.
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="vin-current-limit-slider">VIN Current Limit: <span
|
||||
class="fw-bold text-primary" id="vin-current-limit-value">...</span> A</label>
|
||||
<input class="form-range" id="vin-current-limit-slider" max="8.0" min="0" step="0.1"
|
||||
type="range">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="main-current-limit-slider">Main Current Limit: <span
|
||||
class="fw-bold text-primary" id="main-current-limit-value">...</span> A</label>
|
||||
<input class="form-range" id="main-current-limit-slider" max="7.5" min="0" step="0.1"
|
||||
type="range">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="usb-current-limit-slider">USB Current Limit: <span
|
||||
class="fw-bold text-primary" id="usb-current-limit-value">...</span> A</label>
|
||||
<input class="form-range" id="usb-current-limit-slider" max="4.5" min="0" step="0.1"
|
||||
type="range">
|
||||
</div>
|
||||
<div class="d-flex justify-content-end pt-3 border-top mt-3">
|
||||
<button class="btn btn-primary me-2" id="current-limit-apply-button" type="button">Apply
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="user-settings-pane" role="tabpanel">
|
||||
<form id="user-settings-form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="new-username">New Username</label>
|
||||
<input class="form-control" id="new-username" required type="text">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="new-password">New Password</label>
|
||||
<input class="form-control" id="new-password" required type="password">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="confirm-password">Confirm New Password</label>
|
||||
<input class="form-control" id="confirm-password" required type="password">
|
||||
</div>
|
||||
<div class="d-flex justify-content-end pt-3 border-top mt-3">
|
||||
<button class="btn btn-primary me-2" id="user-settings-apply-button" type="submit">
|
||||
Apply
|
||||
</button>
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="device-settings-pane" role="tabpanel">
|
||||
<div class="mb-3">
|
||||
<div class="mb-3 p-3 border rounded">
|
||||
<label for="baud-rate-select" class="form-label">UART Baud Rate</label>
|
||||
<select class="form-select" id="baud-rate-select">
|
||||
<option value="9600">9600</option>
|
||||
@@ -277,9 +373,24 @@
|
||||
<option value="921600">921600</option>
|
||||
<option value="1500000" selected>1500000</option>
|
||||
</select>
|
||||
<div class="d-flex justify-content-end mt-2">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="baud-rate-apply-button">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 p-3 border rounded">
|
||||
<label for="period-slider" class="form-label">Sensor Period: <span class="fw-bold text-primary" id="period-value">...</span> ms</label>
|
||||
<input type="range" class="form-range" id="period-slider" min="500" max="5000" step="100">
|
||||
<div class="d-flex justify-content-end mt-2">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="period-apply-button">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">System Reboot</label>
|
||||
<p class="text-muted small">This will restart the device. The reboot will occur after 3 seconds.</p>
|
||||
<button type="button" class="btn btn-danger" id="reboot-button">Reboot Now</button>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end pt-3 border-top mt-3">
|
||||
<button type="button" class="btn btn-primary me-2" id="baud-rate-apply-button">Apply</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,7 +415,7 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="wifi-password-connect" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="wifi-password-connect">
|
||||
<input type="password" class="form-control" id="wifi-password-connect" placeholder="Leave blank for open network">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -314,6 +425,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
740
page/package-lock.json
generated
740
page/package-lock.json
generated
@@ -12,14 +12,62 @@
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"chart.js": "^4.4.3"
|
||||
"chart.js": "^4.4.3",
|
||||
"protobufjs": "^7.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"protobufjs-cli": "^1.1.2",
|
||||
"vite": "^7.0.4",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-singlefile": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
|
||||
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.2"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
|
||||
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
|
||||
@@ -436,6 +484,18 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdoc/salty": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz",
|
||||
"integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||
@@ -451,6 +511,60 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
|
||||
@@ -717,6 +831,36 @@
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.9.0.tgz",
|
||||
@@ -730,6 +874,27 @@
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-jsx": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
@@ -745,6 +910,24 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.7",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz",
|
||||
@@ -778,6 +961,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
@@ -790,6 +982,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/catharsis": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz",
|
||||
"integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -852,6 +1056,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
|
||||
@@ -893,6 +1115,112 @@
|
||||
"@esbuild/win32-x64": "0.25.8"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/escodegen": {
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
|
||||
"integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esprima": "^4.0.1",
|
||||
"estraverse": "^4.2.0",
|
||||
"esutils": "^2.0.2",
|
||||
"optionator": "^0.8.1"
|
||||
},
|
||||
"bin": {
|
||||
"escodegen": "bin/escodegen.js",
|
||||
"esgenerate": "bin/esgenerate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"source-map": "~0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/escodegen/node_modules/estraverse": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-visitor-keys": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
||||
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.9.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/estraverse": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
@@ -933,6 +1261,12 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -947,6 +1281,26 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^5.0.1",
|
||||
"once": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -962,6 +1316,23 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -971,6 +1342,44 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js2xmlparser": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
|
||||
"integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"xmlcreate": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdoc": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz",
|
||||
"integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.15",
|
||||
"@jsdoc/salty": "^0.2.1",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"bluebird": "^3.7.2",
|
||||
"catharsis": "^0.9.0",
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
"js2xmlparser": "^4.0.2",
|
||||
"klaw": "^3.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-anchor": "^8.6.7",
|
||||
"marked": "^4.0.10",
|
||||
"mkdirp": "^1.0.4",
|
||||
"requizzle": "^0.2.3",
|
||||
"strip-json-comments": "^3.1.0",
|
||||
"underscore": "~1.13.2"
|
||||
},
|
||||
"bin": {
|
||||
"jsdoc": "jsdoc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
@@ -983,6 +1392,93 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/klaw": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
|
||||
"integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.9"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
|
||||
"integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"prelude-ls": "~1.1.2",
|
||||
"type-check": "~0.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
|
||||
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it-anchor": {
|
||||
"version": "8.6.7",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz",
|
||||
"integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@types/markdown-it": "*",
|
||||
"markdown-it": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
|
||||
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -1008,6 +1504,39 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -1032,6 +1561,32 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
|
||||
"integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"deep-is": "~0.1.3",
|
||||
"fast-levenshtein": "~2.0.6",
|
||||
"levn": "~0.3.0",
|
||||
"prelude-ls": "~1.1.2",
|
||||
"type-check": "~0.3.2",
|
||||
"word-wrap": "~1.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1078,6 +1633,84 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
|
||||
"integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs-cli": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz",
|
||||
"integrity": "sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"escodegen": "^1.13.0",
|
||||
"espree": "^9.0.0",
|
||||
"estraverse": "^5.1.0",
|
||||
"glob": "^8.0.0",
|
||||
"jsdoc": "^4.0.0",
|
||||
"minimist": "^1.2.0",
|
||||
"semver": "^7.1.2",
|
||||
"tmp": "^0.2.1",
|
||||
"uglify-js": "^3.7.7"
|
||||
},
|
||||
"bin": {
|
||||
"pbjs": "bin/pbjs",
|
||||
"pbts": "bin/pbts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"protobufjs": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/requizzle": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz",
|
||||
"integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
|
||||
@@ -1117,6 +1750,28 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1126,6 +1781,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -1154,6 +1821,15 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -1166,6 +1842,47 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||
"integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"prelude-ls": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "3.19.3",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.7",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
|
||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
@@ -1278,6 +1995,27 @@
|
||||
"rollup": "^4.44.1",
|
||||
"vite": "^5.4.11 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/xmlcreate": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz",
|
||||
"integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,23 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"dev": "npm run build:proto && vite",
|
||||
"build": "npm run build:proto && vite build",
|
||||
"preview": "vite preview",
|
||||
"build:proto": "pbjs -t static-module -w es6 -o src/proto.js ../proto/status.proto"
|
||||
},
|
||||
"devDependencies": {
|
||||
"protobufjs-cli": "^1.1.2",
|
||||
"vite": "^7.0.4",
|
||||
"vite-plugin-singlefile": "^2.0.1",
|
||||
"vite-plugin-compression": "^0.5.1"
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-singlefile": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xterm/addon-fit": "^0.9.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"@xterm/addon-fit": "^0.9.0",
|
||||
"chart.js": "^4.4.3"
|
||||
"chart.js": "^4.4.3",
|
||||
"protobufjs": "^7.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
164
page/src/api.js
164
page/src/api.js
@@ -4,15 +4,63 @@
|
||||
* It abstracts the fetch logic, error handling, and JSON parsing for network and control operations.
|
||||
*/
|
||||
|
||||
// Function to get authentication headers
|
||||
export function getAuthHeaders() {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (token) {
|
||||
return { 'Authorization': `Bearer ${token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Global error handler for unauthorized responses
|
||||
export async function handleResponse(response) {
|
||||
if (response.status === 401) {
|
||||
// Unauthorized, log out the user
|
||||
localStorage.removeItem('authToken');
|
||||
// Redirect to login or trigger a logout event
|
||||
// For now, we'll just reload the page, which will trigger the login screen
|
||||
window.location.reload();
|
||||
throw new Error('Unauthorized: Session expired or invalid token.');
|
||||
}
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a user with the provided username and password.
|
||||
* @param {string} username The user's username.
|
||||
* @param {string} password The user's password.
|
||||
* @returns {Promise<Object>} A promise that resolves to the server's JSON response containing a token.
|
||||
* @throws {Error} Throws an error if the authentication fails.
|
||||
*/
|
||||
export async function login(username, password) {
|
||||
const response = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
// Login function does not use handleResponse as it's for obtaining the token
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || `Login failed with status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of available Wi-Fi networks from the server.
|
||||
* @returns {Promise<Array<Object>>} A promise that resolves to an array of Wi-Fi access point objects.
|
||||
* @throws {Error} Throws an error if the network request fails.
|
||||
*/
|
||||
export async function fetchWifiScan() {
|
||||
const response = await fetch('/api/wifi/scan');
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.json();
|
||||
const response = await fetch('/api/wifi/scan', {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
return await handleResponse(response).then(res => res.json());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,16 +71,15 @@ export async function fetchWifiScan() {
|
||||
* @throws {Error} Throws an error if the connection request fails.
|
||||
*/
|
||||
export async function postWifiConnect(ssid, password) {
|
||||
const response = await fetch('/api/setting', { // Updated URL
|
||||
const response = await fetch('/api/setting', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ssid, password }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify({ssid, password}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || `Connection failed with status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
return await handleResponse(response).then(res => res.json());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,35 +89,49 @@ export async function postWifiConnect(ssid, password) {
|
||||
* @throws {Error} Throws an error if the request fails.
|
||||
*/
|
||||
export async function postNetworkSettings(payload) {
|
||||
const response = await fetch('/api/setting', { // Updated URL
|
||||
const response = await fetch('/api/setting', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || `Failed to apply settings with status: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
return await handleResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts the selected UART baud rate to the server.
|
||||
* @param {string} baudrate The selected baud rate.
|
||||
* @returns {Promise<Response>} A promise that resolves to the raw fetch response.
|
||||
* @throws {Error} Throws an error if the request fails.
|
||||
*/
|
||||
export async function postBaudRateSetting(baudrate) {
|
||||
const response = await fetch('/api/setting', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify({ baudrate }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || `Failed to apply baudrate with status: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
return await handleResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts the selected sensor period to the server.
|
||||
* @param {string} period The selected period in milliseconds.
|
||||
* @returns {Promise<Response>} A promise that resolves to the raw fetch response.
|
||||
*/
|
||||
export async function postPeriodSetting(period) {
|
||||
const response = await fetch('/api/setting', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify({ period }),
|
||||
});
|
||||
return await handleResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,9 +140,10 @@ export async function postBaudRateSetting(baudrate) {
|
||||
* @throws {Error} Throws an error if the network request fails.
|
||||
*/
|
||||
export async function fetchSettings() {
|
||||
const response = await fetch('/api/setting'); // Updated URL
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.json();
|
||||
const response = await fetch('/api/setting', {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
return await handleResponse(response).then(res => res.json());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,9 +152,10 @@ export async function fetchSettings() {
|
||||
* @throws {Error} Throws an error if the network request fails.
|
||||
*/
|
||||
export async function fetchControlStatus() {
|
||||
const response = await fetch('/api/control');
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.json();
|
||||
const response = await fetch('/api/control', {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
return await handleResponse(response).then(res => res.json());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,9 +167,42 @@ export async function fetchControlStatus() {
|
||||
export async function postControlCommand(command) {
|
||||
const response = await fetch('/api/control', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify(command)
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return response;
|
||||
return await handleResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the firmware version from the server.
|
||||
* @returns {Promise<Object>} A promise that resolves to an object containing the version.
|
||||
* @throws {Error} Throws an error if the network request fails.
|
||||
*/
|
||||
export async function fetchVersion() {
|
||||
const response = await fetch('/api/version', {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
return await handleResponse(response).then(res => res.json());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user's username and password on the server.
|
||||
* @param {string} newUsername The new username.
|
||||
* @param {string} newPassword The new password.
|
||||
* @returns {Promise<Object>} A promise that resolves to the server's JSON response.
|
||||
* @throws {Error} Throws an error if the update fails.
|
||||
*/
|
||||
export async function updateUserSettings(newUsername, newPassword) {
|
||||
const response = await fetch('/api/setting', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify({new_username: newUsername, new_password: newPassword}),
|
||||
});
|
||||
return await handleResponse(response).then(res => res.json());
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* It handles initialization, theme updates, data updates, and resizing for the three separate charts.
|
||||
*/
|
||||
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { powerChartCtx, voltageChartCtx, currentChartCtx, htmlEl, graphTabPane } from './dom.js';
|
||||
import {Chart, registerables} from 'chart.js';
|
||||
import {currentChartCtx, graphTabPane, htmlEl, powerChartCtx, voltageChartCtx} from './dom.js';
|
||||
|
||||
// Register all necessary Chart.js components
|
||||
Chart.register(...registerables);
|
||||
@@ -17,6 +17,14 @@ export const charts = {
|
||||
current: null
|
||||
};
|
||||
|
||||
// Configuration for dynamic, step-wise Y-axis scaling
|
||||
const scaleConfig = {
|
||||
power: {steps: [5, 20, 50, 160]}, // in Watts
|
||||
voltage: {steps: [5, 10, 15, 25]}, // in Volts
|
||||
current: {steps: [1, 2.5, 5, 10]} // in Amps
|
||||
};
|
||||
|
||||
const channelKeys = ['USB', 'MAIN', 'VIN'];
|
||||
const CHART_DATA_POINTS = 30; // Number of data points to display on the chart
|
||||
|
||||
/**
|
||||
@@ -38,92 +46,82 @@ function initialData() {
|
||||
/**
|
||||
* Creates a common configuration object for a single line chart.
|
||||
* @param {string} title - The title of the chart (e.g., 'Power (W)').
|
||||
* @param {number} minValue - The minimum value for Y-axis.
|
||||
* @param {number} maxValue - The maximum value for Y-axis.
|
||||
* @returns {Object} A Chart.js options object.
|
||||
*/
|
||||
function createChartOptions(title, minValue, maxValue) {
|
||||
function createChartOptions(title) {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
interaction: {mode: 'index', intersect: false},
|
||||
plugins: {
|
||||
legend: { position: 'top' },
|
||||
title: { display: true, text: title }
|
||||
legend: {position: 'top'},
|
||||
title: {display: true, text: title}
|
||||
},
|
||||
scales: {
|
||||
x: { ticks: { autoSkipPadding: 10, maxRotation: 0, minRotation: 0 } },
|
||||
y: {
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
beginAtZero: false,
|
||||
ticks: {
|
||||
stepSize: (maxValue - minValue) / 8
|
||||
}
|
||||
x: {ticks: {autoSkipPadding: 10, maxRotation: 0, minRotation: 0}},
|
||||
y: {
|
||||
min: 0,
|
||||
beginAtZero: true,
|
||||
ticks: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the dataset objects for a chart.
|
||||
* @param {string} unit - The unit for the dataset label (e.g., 'W', 'V', 'A').
|
||||
* @returns {Array<Object>} An array of Chart.js dataset objects.
|
||||
*/
|
||||
function createDatasets(unit) {
|
||||
return channelKeys.map(channel => ({
|
||||
label: `${channel} (${unit})`,
|
||||
data: initialData(),
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
tension: 0.2,
|
||||
pointRadius: 0
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a single chart with its specific configuration.
|
||||
* @param {CanvasRenderingContext2D} context - The canvas context for the chart.
|
||||
* @param {string} title - The chart title.
|
||||
* @param {string} metric - The metric key ('power', 'voltage', 'current').
|
||||
* @param {string} unit - The data unit ('W', 'V', 'A').
|
||||
* @returns {Chart} A new Chart.js instance.
|
||||
*/
|
||||
function initializeSingleChart(context, title, metric, unit) {
|
||||
if (!context) return null;
|
||||
|
||||
const options = createChartOptions(title);
|
||||
const initialMax = scaleConfig[metric].steps[0];
|
||||
options.scales.y.max = initialMax;
|
||||
options.scales.y.ticks.stepSize = initialMax / 5; // Initial step size
|
||||
|
||||
return new Chart(context, {
|
||||
type: 'line',
|
||||
data: {labels: initialLabels(), datasets: createDatasets(unit)},
|
||||
options: options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all three charts (Power, Voltage, Current).
|
||||
* If chart instances already exist, they are destroyed and new ones are created.
|
||||
*/
|
||||
export function initCharts() {
|
||||
// Destroy existing charts if they exist
|
||||
for (const key in charts) {
|
||||
Object.keys(charts).forEach(key => {
|
||||
if (charts[key]) {
|
||||
charts[key].destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create Power Chart
|
||||
if (powerChartCtx) {
|
||||
const powerOptions = createChartOptions('Power', 0, 120);
|
||||
charts.power = new Chart(powerChartCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: initialLabels(),
|
||||
datasets: [
|
||||
{ label: 'Power (W)', data: initialData(), borderWidth: 2, fill: false, tension: 0.2, pointRadius: 2 },
|
||||
{ label: 'Avg Power', data: initialData(), borderWidth: 1.5, borderDash: [10, 5], fill: false, tension: 0, pointRadius: 0 }
|
||||
]
|
||||
},
|
||||
options: powerOptions
|
||||
});
|
||||
}
|
||||
|
||||
// Create Voltage Chart
|
||||
if (voltageChartCtx) {
|
||||
const voltageOptions = createChartOptions('Voltage', 0, 24);
|
||||
charts.voltage = new Chart(voltageChartCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: initialLabels(),
|
||||
datasets: [
|
||||
{ label: 'Voltage (V)', data: initialData(), borderWidth: 2, fill: false, tension: 0.2, pointRadius: 2 },
|
||||
{ label: 'Avg Voltage', data: initialData(), borderWidth: 1.5, borderDash: [10, 5], fill: false, tension: 0, pointRadius: 0 }
|
||||
]
|
||||
},
|
||||
options: voltageOptions
|
||||
});
|
||||
}
|
||||
|
||||
// Create Current Chart
|
||||
if (currentChartCtx) {
|
||||
const currentOptions = createChartOptions('Current', 0, 7);
|
||||
charts.current = new Chart(currentChartCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: initialLabels(),
|
||||
datasets: [
|
||||
{ label: 'Current (A)', data: initialData(), borderWidth: 2, fill: false, tension: 0.2, pointRadius: 2 },
|
||||
{ label: 'Avg Current', data: initialData(), borderWidth: 1.5, borderDash: [10, 5], fill: false, tension: 0, pointRadius: 0 }
|
||||
]
|
||||
},
|
||||
options: currentOptions
|
||||
});
|
||||
}
|
||||
charts.power = initializeSingleChart(powerChartCtx, 'Power', 'power', 'W');
|
||||
charts.voltage = initializeSingleChart(voltageChartCtx, 'Voltage', 'voltage', 'V');
|
||||
charts.current = initializeSingleChart(currentChartCtx, 'Current', 'current', 'A');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,11 +133,13 @@ export function applyChartsTheme(themeName) {
|
||||
const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
const labelColor = isDark ? '#dee2e6' : '#212529';
|
||||
|
||||
const powerColor = getComputedStyle(htmlEl).getPropertyValue('--chart-power-color');
|
||||
const voltageColor = getComputedStyle(htmlEl).getPropertyValue('--chart-voltage-color');
|
||||
const currentColor = getComputedStyle(htmlEl).getPropertyValue('--chart-current-color');
|
||||
const channelColors = [
|
||||
getComputedStyle(htmlEl).getPropertyValue('--chart-usb-color').trim() || '#0d6efd',
|
||||
getComputedStyle(htmlEl).getPropertyValue('--chart-main-color').trim() || '#198754',
|
||||
getComputedStyle(htmlEl).getPropertyValue('--chart-vin-color').trim() || '#dc3545'
|
||||
];
|
||||
|
||||
const updateThemeForChart = (chart, color) => {
|
||||
const updateThemeForChart = (chart) => {
|
||||
if (!chart) return;
|
||||
chart.options.scales.x.grid.color = gridColor;
|
||||
chart.options.scales.y.grid.color = gridColor;
|
||||
@@ -147,64 +147,89 @@ export function applyChartsTheme(themeName) {
|
||||
chart.options.scales.y.ticks.color = labelColor;
|
||||
chart.options.plugins.legend.labels.color = labelColor;
|
||||
chart.options.plugins.title.color = labelColor;
|
||||
chart.data.datasets[0].borderColor = color;
|
||||
chart.data.datasets[1].borderColor = color;
|
||||
chart.data.datasets[1].borderDash = [10, 5];
|
||||
|
||||
chart.data.datasets.forEach((dataset, index) => {
|
||||
dataset.borderColor = channelColors[index];
|
||||
});
|
||||
|
||||
chart.update('none');
|
||||
};
|
||||
|
||||
updateThemeForChart(charts.power, powerColor);
|
||||
updateThemeForChart(charts.voltage, voltageColor);
|
||||
updateThemeForChart(charts.current, currentColor);
|
||||
Object.values(charts).forEach(updateThemeForChart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a single chart with new data and dynamically adjusts its Y-axis scale.
|
||||
* @param {Chart} chart - The Chart.js instance to update.
|
||||
* @param {string} metric - The metric key (e.g., 'power', 'voltage').
|
||||
* @param {Object} data - The new sensor data object.
|
||||
* @param {string} timeLabel - The timestamp label for the new data point.
|
||||
*/
|
||||
function updateSingleChart(chart, metric, data, timeLabel) {
|
||||
if (!chart) return;
|
||||
|
||||
// Shift old data and push new data
|
||||
chart.data.labels.shift();
|
||||
chart.data.labels.push(timeLabel);
|
||||
chart.data.datasets.forEach((dataset, index) => {
|
||||
dataset.data.shift();
|
||||
const channel = channelKeys[index];
|
||||
const value = data[channel]?.[metric];
|
||||
dataset.data.push(value !== undefined ? value.toFixed(2) : null);
|
||||
});
|
||||
|
||||
// --- DYNAMIC STEP-WISE Y-AXIS SCALING ---
|
||||
const config = scaleConfig[metric];
|
||||
if (config?.steps) {
|
||||
const allData = chart.data.datasets
|
||||
.flatMap(dataset => dataset.data)
|
||||
.filter(v => v !== null)
|
||||
.map(v => parseFloat(v));
|
||||
|
||||
const maxDataValue = allData.length > 0 ? Math.max(...allData) : 0;
|
||||
|
||||
// Find the smallest step that is >= maxDataValue
|
||||
let newMax = config.steps.find(step => maxDataValue <= step);
|
||||
|
||||
// If value exceeds all steps, use the largest step. If no data, use the smallest.
|
||||
if (newMax === undefined) {
|
||||
newMax = config.steps[config.steps.length - 1];
|
||||
}
|
||||
|
||||
if (chart.options.scales.y.max !== newMax) {
|
||||
chart.options.scales.y.max = newMax;
|
||||
// Dynamically adjust stepSize for clearer grid lines
|
||||
chart.options.scales.y.ticks.stepSize = newMax / 5;
|
||||
}
|
||||
}
|
||||
// --- END DYNAMIC SCALING ---
|
||||
|
||||
// Update chart only if its tab is visible for performance.
|
||||
if (graphTabPane.classList.contains('show')) {
|
||||
chart.update('none');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates all charts with new sensor data.
|
||||
* @param {Object} data - The new sensor data object from the WebSocket.
|
||||
*/
|
||||
export function updateCharts(data) {
|
||||
const timeLabel = new Date(data.timestamp * 1000).toLocaleTimeString();
|
||||
const timeLabel = new Date(data.timestamp).toLocaleTimeString();
|
||||
|
||||
const updateSingleChart = (chart, value) => {
|
||||
if (!chart) return;
|
||||
|
||||
// Shift old data
|
||||
chart.data.labels.shift();
|
||||
chart.data.datasets.forEach(dataset => dataset.data.shift());
|
||||
|
||||
// Push new data
|
||||
chart.data.labels.push(timeLabel);
|
||||
chart.data.datasets[0].data.push(value.toFixed(2));
|
||||
|
||||
// Calculate average
|
||||
const dataArray = chart.data.datasets[0].data.filter(v => v !== null).map(v => parseFloat(v));
|
||||
if (dataArray.length > 0) {
|
||||
const sum = dataArray.reduce((acc, val) => acc + val, 0);
|
||||
const avg = (sum / dataArray.length).toFixed(2);
|
||||
chart.data.datasets[1].data.push(avg);
|
||||
|
||||
} else {
|
||||
chart.data.datasets[1].data.push(null);
|
||||
}
|
||||
|
||||
// Only update the chart if the tab is visible
|
||||
if (graphTabPane.classList.contains('show')) {
|
||||
chart.update('none');
|
||||
}
|
||||
};
|
||||
|
||||
updateSingleChart(charts.power, data.power);
|
||||
updateSingleChart(charts.voltage, data.voltage);
|
||||
updateSingleChart(charts.current, data.current);
|
||||
updateSingleChart(charts.power, 'power', data, timeLabel);
|
||||
updateSingleChart(charts.voltage, 'voltage', data, timeLabel);
|
||||
updateSingleChart(charts.current, 'current', data, timeLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes all chart canvases. This is typically called on window resize events.
|
||||
*/
|
||||
export function resizeCharts() {
|
||||
for (const key in charts) {
|
||||
if (charts[key]) {
|
||||
charts[key].resize();
|
||||
Object.values(charts).forEach(chart => {
|
||||
if (chart) {
|
||||
chart.resize();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,3 +76,19 @@ export const apPasswordInput = document.getElementById('ap-password');
|
||||
// --- Device Settings Elements ---
|
||||
export const baudRateSelect = document.getElementById('baud-rate-select');
|
||||
export const baudRateApplyButton = document.getElementById('baud-rate-apply-button');
|
||||
export const periodSlider = document.getElementById('period-slider');
|
||||
export const periodValue = document.getElementById('period-value');
|
||||
export const periodApplyButton = document.getElementById('period-apply-button');
|
||||
export const rebootButton = document.getElementById('reboot-button');
|
||||
|
||||
// --- Current Limit Settings Elements ---
|
||||
export const vinSlider = document.getElementById('vin-current-limit-slider');
|
||||
export const vinValueSpan = document.getElementById('vin-current-limit-value');
|
||||
export const mainSlider = document.getElementById('main-current-limit-slider');
|
||||
export const mainValueSpan = document.getElementById('main-current-limit-value');
|
||||
export const usbSlider = document.getElementById('usb-current-limit-slider');
|
||||
export const usbValueSpan = document.getElementById('usb-current-limit-value');
|
||||
export const currentLimitApplyButton = document.getElementById('current-limit-apply-button');
|
||||
|
||||
// --- Footer ---
|
||||
export const versionInfo = document.getElementById('version-info');
|
||||
|
||||
@@ -7,23 +7,59 @@
|
||||
|
||||
import * as dom from './dom.js';
|
||||
import * as api from './api.js';
|
||||
import {getAuthHeaders, handleResponse} from './api.js'; // Import auth functions
|
||||
import * as ui from './ui.js';
|
||||
import { clearTerminal, fitTerminal, downloadTerminalOutput } from './terminal.js';
|
||||
import { debounce, isMobile } from './utils.js';
|
||||
import {clearTerminal, downloadTerminalOutput, fitTerminal} from './terminal.js';
|
||||
import {debounce, isMobile} from './utils.js';
|
||||
|
||||
// A flag to track if charts have been initialized
|
||||
let chartsInitialized = false;
|
||||
let listenersAttached = false;
|
||||
|
||||
// --- Helper functions for settings ---
|
||||
|
||||
function updateSliderValue(slider, span) {
|
||||
if (!slider || !span) return;
|
||||
let value = parseFloat(slider.value).toFixed(1);
|
||||
if (value <= 0) {
|
||||
span.textContent = 'Disabled';
|
||||
} else {
|
||||
span.textContent = `${value} A`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadCurrentLimitSettings() {
|
||||
fetch('/api/setting', {
|
||||
headers: getAuthHeaders(), // Add auth headers
|
||||
})
|
||||
.then(handleResponse) // Handle response for 401
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.vin_current_limit !== undefined) {
|
||||
dom.vinSlider.value = data.vin_current_limit;
|
||||
updateSliderValue(dom.vinSlider, dom.vinValueSpan);
|
||||
}
|
||||
if (data.main_current_limit !== undefined) {
|
||||
dom.mainSlider.value = data.main_current_limit;
|
||||
updateSliderValue(dom.mainSlider, dom.mainValueSpan);
|
||||
}
|
||||
if (data.usb_current_limit !== undefined) {
|
||||
dom.usbSlider.value = data.usb_current_limit;
|
||||
updateSliderValue(dom.usbSlider, dom.usbValueSpan);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error fetching current limit settings:', error));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up all event listeners for the application's interactive elements.
|
||||
* This function is now idempotent and will only attach listeners once.
|
||||
*/
|
||||
export function setupEventListeners() {
|
||||
// --- Theme Toggle ---
|
||||
dom.themeToggle.addEventListener('change', () => {
|
||||
const newTheme = dom.themeToggle.checked ? 'dark' : 'light';
|
||||
localStorage.setItem('theme', newTheme);
|
||||
ui.applyTheme(newTheme);
|
||||
});
|
||||
if (listenersAttached) {
|
||||
console.log("Event listeners already attached. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Terminal Controls ---
|
||||
dom.clearButton.addEventListener('click', clearTerminal);
|
||||
@@ -41,6 +77,68 @@ export function setupEventListeners() {
|
||||
dom.networkApplyButton.addEventListener('click', ui.applyNetworkSettings);
|
||||
dom.apModeApplyButton.addEventListener('click', ui.applyApModeSettings);
|
||||
dom.baudRateApplyButton.addEventListener('click', ui.applyBaudRateSettings);
|
||||
dom.periodApplyButton.addEventListener('click', ui.applyPeriodSettings);
|
||||
|
||||
// --- Device Settings (Reboot & Period Slider) ---
|
||||
if (dom.rebootButton) {
|
||||
dom.rebootButton.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to reboot the device?')) {
|
||||
fetch('/api/reboot', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(), // Add auth headers
|
||||
})
|
||||
.then(handleResponse) // Handle response for 401
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Reboot command sent:', data);
|
||||
ui.hideSettingsModal();
|
||||
alert('Reboot command sent. The device will restart in 3 seconds.');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error sending reboot command:', error);
|
||||
alert('Failed to send reboot command.');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (dom.periodSlider) {
|
||||
dom.periodSlider.addEventListener('input', () => {
|
||||
dom.periodValue.textContent = dom.periodSlider.value;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Current Limit Settings ---
|
||||
dom.vinSlider.addEventListener('input', () => updateSliderValue(dom.vinSlider, dom.vinValueSpan));
|
||||
dom.mainSlider.addEventListener('input', () => updateSliderValue(dom.mainSlider, dom.mainValueSpan));
|
||||
dom.usbSlider.addEventListener('input', () => updateSliderValue(dom.usbSlider, dom.usbValueSpan));
|
||||
|
||||
dom.currentLimitApplyButton.addEventListener('click', () => {
|
||||
const settings = {
|
||||
vin_current_limit: parseFloat(dom.vinSlider.value),
|
||||
main_current_limit: parseFloat(dom.mainSlider.value),
|
||||
usb_current_limit: parseFloat(dom.usbSlider.value)
|
||||
};
|
||||
|
||||
fetch('/api/setting', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(), // Add auth headers
|
||||
},
|
||||
body: JSON.stringify(settings),
|
||||
})
|
||||
.then(handleResponse) // Handle response for 401
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Current limit settings applied:', data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error applying current limit settings:', error);
|
||||
alert('Failed to apply current limit settings.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// --- Settings Modal Toggles (for showing/hiding sections) ---
|
||||
dom.apModeToggle.addEventListener('change', () => {
|
||||
@@ -54,7 +152,12 @@ export function setupEventListeners() {
|
||||
// --- General App Listeners ---
|
||||
dom.settingsButton.addEventListener('click', ui.initializeSettings);
|
||||
|
||||
// --- Accessibility: Remove focus from modal elements before hiding ---
|
||||
// --- Accessibility & Modal Events ---
|
||||
dom.settingsModal.addEventListener('show.bs.modal', () => {
|
||||
// Load settings when the modal is about to be shown
|
||||
loadCurrentLimitSettings();
|
||||
});
|
||||
|
||||
const blurActiveElement = () => {
|
||||
if (document.activeElement && typeof document.activeElement.blur === 'function') {
|
||||
document.activeElement.blur();
|
||||
@@ -90,4 +193,6 @@ export function setupEventListeners() {
|
||||
// --- Window Resize Event ---
|
||||
// Debounced to avoid excessive calls during resizing.
|
||||
window.addEventListener('resize', debounce(ui.handleResize, 150));
|
||||
|
||||
listenersAttached = true;
|
||||
}
|
||||
|
||||
391
page/src/main.js
391
page/src/main.js
@@ -10,100 +10,367 @@ import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css';
|
||||
import './style.css';
|
||||
|
||||
// --- Module Imports ---
|
||||
import { initWebSocket } from './websocket.js';
|
||||
import { setupTerminal, term } from './terminal.js';
|
||||
// --- Module Imports -- -
|
||||
import {StatusMessage} from './proto.js';
|
||||
import * as api from './api.js';
|
||||
import {initWebSocket} from './websocket.js';
|
||||
import {setupTerminal, term} from './terminal.js';
|
||||
import {
|
||||
applyTheme,
|
||||
initUI,
|
||||
updateControlStatus,
|
||||
updateSensorUI,
|
||||
updateWifiStatusUI,
|
||||
updateWebsocketStatus
|
||||
updateSwitchStatusUI,
|
||||
updateUptimeUI,
|
||||
updateVersionUI,
|
||||
updateWebsocketStatus,
|
||||
updateWifiStatusUI
|
||||
} from './ui.js';
|
||||
import { setupEventListeners } from './events.js';
|
||||
import {setupEventListeners} from './events.js';
|
||||
|
||||
// --- Globals ---
|
||||
// StatusMessage is imported directly from the generated proto.js file.
|
||||
let isRecording = false;
|
||||
let recordedData = [];
|
||||
|
||||
// --- DOM Elements ---
|
||||
const loginContainer = document.getElementById('login-container');
|
||||
const mainContent = document.querySelector('main.container');
|
||||
const loginForm = document.getElementById('login-form');
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const loginAlert = document.getElementById('login-alert');
|
||||
const logoutButton = document.getElementById('logout-button');
|
||||
const themeToggleLogin = document.getElementById('theme-toggle-login');
|
||||
const themeIconLogin = document.getElementById('theme-icon-login');
|
||||
const themeToggleMain = document.getElementById('theme-toggle');
|
||||
const themeIconMain = document.getElementById('theme-icon');
|
||||
|
||||
// User Settings DOM Elements
|
||||
const userSettingsForm = document.getElementById('user-settings-form');
|
||||
const newUsernameInput = document.getElementById('new-username');
|
||||
const newPasswordInput = document.getElementById('new-password');
|
||||
const confirmPasswordInput = document.getElementById('confirm-password');
|
||||
|
||||
// Metrics Tab DOM Elements
|
||||
const recordButton = document.getElementById('record-button');
|
||||
const stopButton = document.getElementById('stop-button');
|
||||
const downloadCsvButton = document.getElementById('download-csv-button');
|
||||
|
||||
|
||||
// --- WebSocket Event Handlers ---
|
||||
|
||||
/**
|
||||
* Callback function for when the WebSocket connection is successfully opened.
|
||||
* Updates the UI to show an 'Online' status and fetches the initial control status.
|
||||
*/
|
||||
function onWsOpen() {
|
||||
updateWebsocketStatus(true);
|
||||
if (term) {
|
||||
term.write('\x1b[32mConnected to WebSocket Server\x1b[0m\r\n');
|
||||
}
|
||||
updateControlStatus();
|
||||
console.log('Connected to WebSocket Server');
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for when the WebSocket connection is closed.
|
||||
* Updates the UI to show an 'Offline' status and attempts to reconnect after a delay.
|
||||
*/
|
||||
function onWsClose() {
|
||||
updateWebsocketStatus(false);
|
||||
if (term) {
|
||||
term.write('\r\n\x1b[31mConnection closed. Reconnecting...\x1b[0m\r\n');
|
||||
}
|
||||
// Attempt to re-establish the connection after 2 seconds
|
||||
setTimeout(initialize, 2000);
|
||||
console.warn('Connection closed. Reconnecting...');
|
||||
setTimeout(connect, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for when a message is received from the WebSocket server.
|
||||
* It handles both JSON messages (for sensor and status updates) and binary data (for the terminal).
|
||||
* Callback for when a message is received from the WebSocket server.
|
||||
* @param {MessageEvent} event - The WebSocket message event.
|
||||
*/
|
||||
function onWsMessage(event) {
|
||||
if (typeof event.data === 'string') {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === 'sensor_data') {
|
||||
updateSensorUI(message);
|
||||
} else if (message.type === 'wifi_status') {
|
||||
updateWifiStatusUI(message);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore non-JSON string messages
|
||||
}
|
||||
} else if (term && event.data instanceof ArrayBuffer) {
|
||||
// Write raw UART data to the terminal
|
||||
const data = new Uint8Array(event.data);
|
||||
term.write(data);
|
||||
if (!(event.data instanceof ArrayBuffer)) {
|
||||
console.warn('Message is not an ArrayBuffer, skipping protobuf decoding.');
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = new Uint8Array(event.data);
|
||||
try {
|
||||
const decodedMessage = StatusMessage.decode(buffer);
|
||||
const payloadType = decodedMessage.payload;
|
||||
|
||||
switch (payloadType) {
|
||||
case 'sensorData': {
|
||||
const sensorData = decodedMessage.sensorData;
|
||||
if (sensorData) {
|
||||
// Create a payload for the sensor UI (charts and header)
|
||||
const sensorPayload = {
|
||||
USB: sensorData.usb,
|
||||
MAIN: sensorData.main,
|
||||
VIN: sensorData.vin,
|
||||
timestamp: sensorData.timestampMs,
|
||||
uptime: sensorData.uptimeMs
|
||||
};
|
||||
updateSensorUI(sensorPayload);
|
||||
|
||||
if (isRecording) {
|
||||
recordedData.push(sensorPayload);
|
||||
}
|
||||
|
||||
// Update uptime separately from the sensor data payload
|
||||
if (sensorData.uptimeMs !== undefined) {
|
||||
updateUptimeUI(sensorData.uptimeMs / 1000);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'wifiStatus':
|
||||
updateWifiStatusUI(decodedMessage.wifiStatus);
|
||||
break;
|
||||
|
||||
case 'swStatus':
|
||||
if (decodedMessage.swStatus) {
|
||||
updateSwitchStatusUI(decodedMessage.swStatus);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'uartData':
|
||||
if (term && decodedMessage.uartData && decodedMessage.uartData.data) {
|
||||
term.write(decodedMessage.uartData.data);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (payloadType !== undefined) {
|
||||
console.warn('Received message with unknown or empty payload type:', payloadType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error decoding protobuf message:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Authentication Functions ---
|
||||
|
||||
function checkAuth() {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (token) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
const username = usernameInput.value;
|
||||
const password = passwordInput.value;
|
||||
|
||||
try {
|
||||
const response = await api.login(username, password);
|
||||
if (response && response.token) {
|
||||
localStorage.setItem('authToken', response.token);
|
||||
loginAlert.classList.add('d-none');
|
||||
loginContainer.style.setProperty('display', 'none', 'important');
|
||||
initializeMainAppContent(); // After successful login, initialize the main app
|
||||
} else {
|
||||
loginAlert.textContent = 'Login failed: No token received.';
|
||||
loginAlert.classList.remove('d-none');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
loginAlert.textContent = `Login failed: ${error.message}`;
|
||||
loginAlert.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('authToken');
|
||||
// Hide main content and show login form
|
||||
loginContainer.style.setProperty('display', 'flex', 'important');
|
||||
mainContent.style.setProperty('display', 'none', 'important');
|
||||
// Optionally, disconnect WebSocket or perform other cleanup
|
||||
// For now, just hide the main content.
|
||||
}
|
||||
|
||||
// --- User Settings Functions ---
|
||||
async function handleUserSettingsSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const newUsername = newUsernameInput.value;
|
||||
const newPassword = newPasswordInput.value;
|
||||
const confirmPassword = confirmPasswordInput.value;
|
||||
|
||||
if (!newUsername || !newPassword || !confirmPassword) {
|
||||
alert('Please fill in all fields for username and password.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert('New password and confirm password do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.updateUserSettings(newUsername, newPassword);
|
||||
if (response && response.status === 'user_credentials_updated') {
|
||||
alert('Username and password updated successfully. Please log in again with new credentials.');
|
||||
handleLogout(); // Force logout to re-authenticate with new credentials
|
||||
} else {
|
||||
alert(`Failed to update credentials: ${response.message || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user settings:', error);
|
||||
alert(`Error updating user settings: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Theme Toggle Functions ---
|
||||
function setupThemeToggles() {
|
||||
// Initialize theme for login page
|
||||
const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
applyTheme(savedTheme);
|
||||
themeToggleLogin.checked = savedTheme === 'dark';
|
||||
themeIconLogin.className = savedTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
|
||||
|
||||
// Sync main theme toggle with login theme toggle (initial state)
|
||||
themeToggleMain.checked = savedTheme === 'dark';
|
||||
themeIconMain.className = savedTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
|
||||
|
||||
themeToggleLogin.addEventListener('change', () => {
|
||||
const newTheme = themeToggleLogin.checked ? 'dark' : 'light';
|
||||
applyTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
themeIconLogin.className = newTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
|
||||
themeToggleMain.checked = themeToggleLogin.checked; // Keep main toggle in sync
|
||||
themeIconMain.className = themeIconLogin.className; // Keep main icon in sync
|
||||
});
|
||||
|
||||
themeToggleMain.addEventListener('change', () => {
|
||||
const newTheme = themeToggleMain.checked ? 'dark' : 'light';
|
||||
applyTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
themeIconMain.className = newTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
|
||||
themeToggleLogin.checked = themeToggleMain.checked; // Keep login toggle in sync
|
||||
themeIconLogin.className = themeIconMain.className; // Keep login icon in sync
|
||||
});
|
||||
}
|
||||
|
||||
// --- Recording and Downloading Functions ---
|
||||
|
||||
function startRecording() {
|
||||
isRecording = true;
|
||||
recordedData = [];
|
||||
recordButton.style.display = 'none';
|
||||
stopButton.style.display = 'inline-block';
|
||||
downloadCsvButton.style.display = 'none';
|
||||
console.log('Recording started.');
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
isRecording = false;
|
||||
recordButton.style.display = 'inline-block';
|
||||
stopButton.style.display = 'none';
|
||||
if (recordedData.length > 0) {
|
||||
downloadCsvButton.style.display = 'inline-block';
|
||||
}
|
||||
console.log('Recording stopped. Data points captured:', recordedData.length);
|
||||
}
|
||||
|
||||
function downloadCSV() {
|
||||
if (recordedData.length === 0) {
|
||||
alert('No data to download.');
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = [
|
||||
'timestamp', 'uptime_ms',
|
||||
'vin_voltage', 'vin_current', 'vin_power',
|
||||
'main_voltage', 'main_current', 'main_power',
|
||||
'usb_voltage', 'usb_current', 'usb_power'
|
||||
];
|
||||
const csvRows = [headers.join(',')];
|
||||
|
||||
recordedData.forEach(data => {
|
||||
const timestamp = new Date(data.timestamp).toISOString();
|
||||
const row = [
|
||||
timestamp,
|
||||
data.uptime,
|
||||
Number(data.VIN.voltage).toFixed(3), Number(data.VIN.current).toFixed(3), Number(data.VIN.power).toFixed(3),
|
||||
Number(data.MAIN.voltage).toFixed(3), Number(data.MAIN.current).toFixed(3), Number(data.MAIN.power).toFixed(3),
|
||||
Number(data.USB.voltage).toFixed(3), Number(data.USB.current).toFixed(3), Number(data.USB.power).toFixed(3)
|
||||
];
|
||||
csvRows.push(row.join(','));
|
||||
});
|
||||
|
||||
const blob = new Blob([csvRows.join('\n')], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const now = new Date();
|
||||
const pad = (num) => num.toString().padStart(2, '0');
|
||||
const datePart = `${now.getFullYear().toString().slice(-2)}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
||||
const timePart = `${pad(now.getHours())}-${pad(now.getMinutes())}`;
|
||||
const filename = `powermate_${datePart}_${timePart}.csv`;
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', filename);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
// --- Application Initialization ---
|
||||
|
||||
/**
|
||||
* Initializes the entire application.
|
||||
* This function sets up the UI, theme, terminal, chart, WebSocket connection, and event listeners.
|
||||
*/
|
||||
function initialize() {
|
||||
// Initialize basic UI components
|
||||
async function initializeVersion() {
|
||||
try {
|
||||
const versionData = await api.fetchVersion();
|
||||
if (versionData && versionData.version) {
|
||||
updateVersionUI(versionData.version);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching version:', error);
|
||||
updateVersionUI('N/A');
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
updateControlStatus();
|
||||
initWebSocket({ onOpen: onWsOpen, onClose: onWsClose, onMessage: onWsMessage });
|
||||
}
|
||||
|
||||
// New function to initialize main app content after successful login or on initial load if authenticated
|
||||
function initializeMainAppContent() {
|
||||
loginContainer.style.setProperty('display', 'none', 'important');
|
||||
mainContent.style.setProperty('display', 'block', 'important');
|
||||
|
||||
initUI();
|
||||
|
||||
// Set up the interactive components first
|
||||
setupTerminal();
|
||||
initializeVersion();
|
||||
setupEventListeners(); // Attach main app event listeners
|
||||
logoutButton.addEventListener('click', handleLogout); // Attach logout listener
|
||||
|
||||
// Attach listeners for recording/downloading
|
||||
recordButton.addEventListener('click', startRecording);
|
||||
stopButton.addEventListener('click', stopRecording);
|
||||
downloadCsvButton.addEventListener('click', downloadCSV);
|
||||
|
||||
// Apply the saved theme or detect the user's preferred theme.
|
||||
// This must be done AFTER the chart and terminal are initialized.
|
||||
const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
applyTheme(savedTheme);
|
||||
connect();
|
||||
|
||||
// Establish the WebSocket connection with the defined handlers
|
||||
initWebSocket({
|
||||
onOpen: onWsOpen,
|
||||
onClose: onWsClose,
|
||||
onMessage: onWsMessage
|
||||
});
|
||||
// Attach user settings form listener
|
||||
if (userSettingsForm) {
|
||||
userSettingsForm.addEventListener('submit', handleUserSettingsSubmit);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach all event listeners to the DOM elements
|
||||
setupEventListeners();
|
||||
function initialize() {
|
||||
setupThemeToggles(); // Setup theme toggles for both login and main (initial sync)
|
||||
|
||||
// Always attach login form listener
|
||||
loginForm.addEventListener('submit', handleLogin);
|
||||
|
||||
if (!checkAuth()) { // If NOT authenticated
|
||||
// Show login form
|
||||
loginContainer.style.setProperty('display', 'flex', 'important');
|
||||
mainContent.style.setProperty('display', 'none', 'important');
|
||||
console.log('Not authenticated. Login form displayed. Main app content NOT initialized.');
|
||||
return; // IMPORTANT: Stop execution here if not authenticated
|
||||
}
|
||||
|
||||
// If authenticated, initialize main content
|
||||
console.log('Authenticated. Initializing main app content.');
|
||||
initializeMainAppContent();
|
||||
}
|
||||
|
||||
// --- Start Application ---
|
||||
|
||||
// Wait for the DOM to be fully loaded before initializing the application.
|
||||
document.addEventListener('DOMContentLoaded', initialize);
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
|
||||
:root {
|
||||
--bs-body-font-family: 'Courier New', Courier, monospace;
|
||||
--chart-power-color: #007bff;
|
||||
--chart-voltage-color: #28a745;
|
||||
--chart-current-color: #ffc107;
|
||||
/* Chart Channel Colors */
|
||||
--chart-usb-color: #0d6efd; /* Bootstrap Blue */
|
||||
--chart-main-color: #198754; /* Bootstrap Green */
|
||||
--chart-vin-color: #dc3545; /* Bootstrap Red */
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
--chart-power-color: #569cd6;
|
||||
--chart-voltage-color: #4ec9b0;
|
||||
--chart-current-color: #dcdcaa;
|
||||
/* Chart Channel Colors for Dark Theme */
|
||||
--chart-usb-color: #569cd6; /* A lighter blue for dark backgrounds */
|
||||
--chart-main-color: #4ec9b0; /* A teal/cyan for dark backgrounds */
|
||||
--chart-vin-color: #d16969; /* A softer red for dark backgrounds */
|
||||
}
|
||||
|
||||
body, .card, .modal-content, .list-group-item, .nav-tabs .nav-link {
|
||||
@@ -112,6 +114,10 @@ footer a {
|
||||
|
||||
/* Mobile Optimizations */
|
||||
@media (max-width: 767.98px) {
|
||||
#login-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
* theme handling, and data communication with the WebSocket.
|
||||
*/
|
||||
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import {Terminal} from '@xterm/xterm';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { terminalContainer } from './dom.js';
|
||||
import { isMobile } from './utils.js';
|
||||
import { websocket, sendWebsocketMessage } from './websocket.js';
|
||||
import {FitAddon} from '@xterm/addon-fit';
|
||||
import {terminalContainer} from './dom.js';
|
||||
import {isMobile} from './utils.js';
|
||||
import {sendWebsocketMessage} from './websocket.js';
|
||||
|
||||
// Exported terminal instance and addon for global access
|
||||
export let term;
|
||||
@@ -41,7 +41,7 @@ export function setupTerminal() {
|
||||
}
|
||||
|
||||
fitAddon = new FitAddon();
|
||||
term = new Terminal({ convertEol: true, cursorBlink: true });
|
||||
term = new Terminal({convertEol: true, cursorBlink: true});
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(terminalContainer);
|
||||
|
||||
@@ -114,7 +114,7 @@ export function downloadTerminalOutput() {
|
||||
}
|
||||
|
||||
// Create a blob from the text content
|
||||
const blob = new Blob([fullText], { type: 'text/plain;charset=utf-8' });
|
||||
const blob = new Blob([fullText], {type: 'text/plain;charset=utf-8'});
|
||||
|
||||
// Create a link element to trigger the download
|
||||
const link = document.createElement('a');
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import * as dom from './dom.js';
|
||||
import * as api from './api.js';
|
||||
import { formatUptime, isMobile } from './utils.js';
|
||||
import { applyTerminalTheme, fitTerminal } from './terminal.js';
|
||||
import { applyChartsTheme, resizeCharts, updateCharts } from './chart.js';
|
||||
import {formatUptime, isMobile} from './utils.js';
|
||||
import {applyTerminalTheme, fitTerminal} from './terminal.js';
|
||||
import {applyChartsTheme, resizeCharts, updateCharts} from './chart.js';
|
||||
|
||||
// Instance of the Bootstrap Modal for Wi-Fi connection
|
||||
let wifiModal;
|
||||
@@ -40,21 +40,49 @@ export function applyTheme(themeName) {
|
||||
* @param {Object} data - The sensor data object from the WebSocket.
|
||||
*/
|
||||
export function updateSensorUI(data) {
|
||||
dom.voltageDisplay.textContent = `${data.voltage.toFixed(2)} V`;
|
||||
dom.currentDisplay.textContent = `${data.current.toFixed(2)} A`;
|
||||
dom.powerDisplay.textContent = `${data.power.toFixed(2)} W`;
|
||||
if (data.uptime_sec !== undefined) {
|
||||
dom.uptimeDisplay.textContent = formatUptime(data.uptime_sec);
|
||||
// Display VIN channel data in the header as a primary overview
|
||||
if (data.VIN) {
|
||||
dom.voltageDisplay.textContent = `${data.VIN.voltage.toFixed(2)} V`;
|
||||
dom.currentDisplay.textContent = `${data.VIN.current.toFixed(2)} A`;
|
||||
dom.powerDisplay.textContent = `${data.VIN.power.toFixed(2)} W`;
|
||||
}
|
||||
|
||||
// Pass the entire multi-channel data object to the charts
|
||||
updateCharts(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the system uptime display in the UI.
|
||||
* @param {number} uptimeInSeconds - The system uptime in seconds.
|
||||
*/
|
||||
export function updateUptimeUI(uptimeInSeconds) {
|
||||
if (uptimeInSeconds !== undefined) {
|
||||
dom.uptimeDisplay.textContent = formatUptime(uptimeInSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the power switch toggle states based on WebSocket data.
|
||||
* @param {Object} swStatus - The switch status object from the WebSocket message.
|
||||
*/
|
||||
export function updateSwitchStatusUI(swStatus) {
|
||||
if (swStatus) {
|
||||
if (swStatus.main !== undefined) {
|
||||
dom.mainPowerToggle.checked = swStatus.main;
|
||||
}
|
||||
if (swStatus.usb !== undefined) {
|
||||
dom.usbPowerToggle.checked = swStatus.usb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Wi-Fi status indicator in the header.
|
||||
* @param {Object} data - The Wi-Fi status object from the WebSocket.
|
||||
*/
|
||||
export function updateWifiStatusUI(data) {
|
||||
if (data.connected) {
|
||||
// Update header status
|
||||
dom.wifiSsidStatus.textContent = data.ssid;
|
||||
dom.wifiStatus.title = `Signal Strength: ${data.rssi} dBm`;
|
||||
let iconClass = 'bi me-2 ';
|
||||
@@ -64,12 +92,31 @@ export function updateWifiStatusUI(data) {
|
||||
dom.wifiIcon.className = iconClass;
|
||||
dom.wifiStatus.classList.replace('text-muted', 'text-success');
|
||||
dom.wifiStatus.classList.remove('text-danger');
|
||||
|
||||
// Update settings modal
|
||||
dom.currentWifiSsid.textContent = data.ssid;
|
||||
dom.currentWifiIp.textContent = `IP Address: ${data.ipAddress || 'N/A'}`;
|
||||
} else {
|
||||
// Update header status
|
||||
dom.wifiSsidStatus.textContent = 'Disconnected';
|
||||
dom.wifiStatus.title = '';
|
||||
dom.wifiIcon.className = 'bi bi-wifi-off me-2';
|
||||
dom.wifiStatus.classList.replace('text-success', 'text-muted');
|
||||
dom.wifiStatus.classList.remove('text-danger');
|
||||
|
||||
// Update settings modal
|
||||
dom.currentWifiSsid.textContent = 'Not Connected';
|
||||
dom.currentWifiIp.textContent = 'IP Address: -';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the version information in the footer.
|
||||
* @param {string} version - The firmware version string.
|
||||
*/
|
||||
export function updateVersionUI(version) {
|
||||
if (version) {
|
||||
dom.versionInfo.textContent = `${version}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +156,7 @@ export async function scanForWifi() {
|
||||
wifiModal.show();
|
||||
dom.wifiModalEl.addEventListener('shown.bs.modal', () => {
|
||||
dom.wifiPasswordConnectInput.focus();
|
||||
}, { once: true });
|
||||
}, {once: true});
|
||||
});
|
||||
|
||||
dom.wifiApList.appendChild(row);
|
||||
@@ -178,10 +225,10 @@ export async function applyNetworkSettings() {
|
||||
return;
|
||||
}
|
||||
|
||||
payload = { net_type: 'static', ip, gateway, subnet, dns1 };
|
||||
payload = {net_type: 'static', ip, gateway, subnet, dns1};
|
||||
if (dns2) payload.dns2 = dns2;
|
||||
} else {
|
||||
payload = { net_type: 'dhcp' };
|
||||
payload = {net_type: 'dhcp'};
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -202,7 +249,7 @@ export async function applyNetworkSettings() {
|
||||
*/
|
||||
export async function applyApModeSettings() {
|
||||
const mode = dom.apModeToggle.checked ? 'apsta' : 'sta';
|
||||
let payload = { mode };
|
||||
let payload = {mode};
|
||||
|
||||
dom.apModeApplyButton.disabled = true;
|
||||
dom.apModeApplyButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Applying...`;
|
||||
@@ -254,6 +301,24 @@ export async function applyBaudRateSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the selected sensor period by sending it to the server.
|
||||
*/
|
||||
export async function applyPeriodSettings() {
|
||||
const period = dom.periodSlider.value;
|
||||
dom.periodApplyButton.disabled = true;
|
||||
dom.periodApplyButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Applying...`;
|
||||
|
||||
try {
|
||||
await api.postPeriodSetting(period);
|
||||
} catch (error) {
|
||||
console.error('Error applying period:', error);
|
||||
} finally {
|
||||
dom.periodApplyButton.disabled = false;
|
||||
dom.periodApplyButton.innerHTML = 'Apply';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and displays the current network and device settings in the settings modal.
|
||||
*/
|
||||
@@ -291,6 +356,10 @@ export async function initializeSettings() {
|
||||
if (data.baudrate) {
|
||||
dom.baudRateSelect.value = data.baudrate;
|
||||
}
|
||||
if (data.period) {
|
||||
dom.periodSlider.value = data.period;
|
||||
dom.periodValue.textContent = data.period;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing settings:', error);
|
||||
|
||||
@@ -20,18 +20,19 @@ export function debounce(func, delay) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration in total seconds into a human-readable string (e.g., "1d 02:30:15").
|
||||
* Formats a duration in total seconds into a human-readable string (e.g., "2days 02:30:15").
|
||||
* @param {number} totalSeconds The total seconds to format.
|
||||
* @returns {string} The formatted uptime string.
|
||||
*/
|
||||
export function formatUptime(totalSeconds) {
|
||||
totalSeconds = Math.floor(totalSeconds);
|
||||
const days = Math.floor(totalSeconds / 86400);
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
const pad = (num) => String(num).padStart(2, '0');
|
||||
const timeString = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
||||
return days > 0 ? `${days}d ${timeString}` : timeString;
|
||||
return days > 0 ? `${days}days ${timeString}` : timeString;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,32 +2,116 @@
|
||||
* @file websocket.js
|
||||
* @description This module handles the WebSocket connection for real-time, two-way
|
||||
* communication with the server. It provides functions to initialize the connection
|
||||
* and send messages.
|
||||
* and send messages, including a heartbeat mechanism to detect disconnections.
|
||||
*/
|
||||
|
||||
// The WebSocket instance, exported for potential direct access if needed.
|
||||
export let websocket;
|
||||
|
||||
// The WebSocket server address, derived from the current page's hostname.
|
||||
const gateway = `ws://${window.location.hostname}/ws`;
|
||||
// The WebSocket server address, derived from the current page's host (hostname + port).
|
||||
const baseGateway = `ws://${window.location.host}/ws`;
|
||||
|
||||
// Heartbeat related variables
|
||||
let pingIntervalId = null;
|
||||
let pongTimeoutId = null;
|
||||
const HEARTBEAT_INTERVAL = 10000; // 10 seconds: How often to send a 'ping'
|
||||
const HEARTBEAT_TIMEOUT = 5000; // 5 seconds: How long to wait for a 'pong' after sending a 'ping'
|
||||
|
||||
/**
|
||||
* Initializes the WebSocket connection and sets up event handlers.
|
||||
* @param {Object} callbacks - An object containing callback functions for WebSocket events.
|
||||
* @param {function} callbacks.onOpen - Called when the connection is successfully opened.
|
||||
* @param {function} callbacks.onClose - Called when the connection is closed.
|
||||
* @param {function} callbacks.onMessage - Called when a message is received from the server.
|
||||
* Starts the heartbeat mechanism.
|
||||
* Sends a 'ping' message to the server at regular intervals and sets a timeout
|
||||
* to detect if a 'pong' response is not received.
|
||||
*/
|
||||
export function initWebSocket({ onOpen, onClose, onMessage }) {
|
||||
function startHeartbeat() {
|
||||
stopHeartbeat(); // Ensure any previous heartbeat is stopped before starting a new one
|
||||
|
||||
pingIntervalId = setInterval(() => {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send('ping');
|
||||
|
||||
// Set a timeout to check if a pong is received within HEARTBEAT_TIMEOUT
|
||||
pongTimeoutId = setTimeout(() => {
|
||||
console.warn('WebSocket: No pong received within timeout, closing connection.');
|
||||
// If no pong is received, close the connection. This will trigger the onClose handler.
|
||||
websocket.close();
|
||||
}, HEARTBEAT_TIMEOUT);
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the heartbeat mechanism by clearing the ping interval and pong timeout.
|
||||
*/
|
||||
function stopHeartbeat() {
|
||||
if (pingIntervalId) {
|
||||
clearInterval(pingIntervalId);
|
||||
pingIntervalId = null;
|
||||
}
|
||||
if (pongTimeoutId) {
|
||||
clearTimeout(pongTimeoutId);
|
||||
pongTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the WebSocket connection and sets up event handlers, including a heartbeat mechanism.
|
||||
* @param {Object} callbacks - An object containing callback functions for WebSocket events.
|
||||
* @param {function} [callbacks.onOpen] - Called when the connection is successfully opened.
|
||||
* @param {function} [callbacks.onClose] - Called when the connection is closed.
|
||||
* @param {function} [callbacks.onMessage] - Called when a message is received from the server (excluding 'pong' messages).
|
||||
* @param {function} [callbacks.onError] - Called when an error occurs with the WebSocket connection.
|
||||
*/
|
||||
export function initWebSocket({onOpen, onClose, onMessage, onError}) {
|
||||
const token = localStorage.getItem('authToken');
|
||||
let gateway = baseGateway;
|
||||
|
||||
if (token) {
|
||||
gateway = `${baseGateway}?token=${token}`;
|
||||
}
|
||||
|
||||
console.log(`Trying to open a WebSocket connection to ${gateway}...`);
|
||||
websocket = new WebSocket(gateway);
|
||||
// Set binary type to arraybuffer to handle raw binary data from the UART.
|
||||
websocket.binaryType = "arraybuffer";
|
||||
|
||||
// Assign event handlers from the provided callbacks
|
||||
if (onOpen) websocket.onopen = onOpen;
|
||||
if (onClose) websocket.onclose = onClose;
|
||||
if (onMessage) websocket.onmessage = onMessage;
|
||||
// Assign event handlers, wrapping user-provided callbacks to include heartbeat logic
|
||||
websocket.onopen = (event) => {
|
||||
console.log('WebSocket connection opened.');
|
||||
startHeartbeat(); // Start heartbeat on successful connection
|
||||
if (onOpen) {
|
||||
onOpen(event);
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onclose = (event) => {
|
||||
console.log('WebSocket connection closed:', event);
|
||||
stopHeartbeat(); // Stop heartbeat when connection closes
|
||||
if (onClose) {
|
||||
onClose(event);
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onmessage = (event) => {
|
||||
if (event.data === 'pong') {
|
||||
// Clear the timeout as pong was received, resetting for the next ping
|
||||
clearTimeout(pongTimeoutId);
|
||||
pongTimeoutId = null;
|
||||
} else {
|
||||
// If it's not a pong message, pass it to the user's onMessage callback
|
||||
if (onMessage) {
|
||||
onMessage(event);
|
||||
} else {
|
||||
console.log('WebSocket message received:', event.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,5 +121,7 @@ export function initWebSocket({ onOpen, onClose, onMessage }) {
|
||||
export function sendWebsocketMessage(data) {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send(data);
|
||||
} else {
|
||||
console.warn('WebSocket is not open. Message not sent:', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,3 @@
|
||||
nvs,data,nvs,0x9000,24K,
|
||||
phy_init,data,phy,0xf000,4K,
|
||||
factory,app,factory,0x10000,2M,
|
||||
littlefs, data, littlefs, ,1536K,
|
||||
|
46
proto/status.proto
Normal file
46
proto/status.proto
Normal file
@@ -0,0 +1,46 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// Represents data for a single sensor channel
|
||||
message SensorChannelData {
|
||||
float voltage = 1;
|
||||
float current = 2;
|
||||
float power = 3;
|
||||
}
|
||||
|
||||
// Contains data for all sensor channels and system info
|
||||
message SensorData {
|
||||
SensorChannelData usb = 1;
|
||||
SensorChannelData main = 2;
|
||||
SensorChannelData vin = 3;
|
||||
uint64 timestamp_ms = 4;
|
||||
uint64 uptime_ms = 5;
|
||||
}
|
||||
|
||||
// Contains WiFi connection status
|
||||
message WifiStatus {
|
||||
bool connected = 1;
|
||||
string ssid = 2;
|
||||
int32 rssi = 3;
|
||||
string ip_address = 4;
|
||||
}
|
||||
|
||||
// Contains raw UART data
|
||||
message UartData {
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
// Contains load sw status
|
||||
message LoadSwStatus {
|
||||
bool main = 1;
|
||||
bool usb = 2;
|
||||
}
|
||||
|
||||
// Top-level message for all websocket communication
|
||||
message StatusMessage {
|
||||
oneof payload {
|
||||
SensorData sensor_data = 1;
|
||||
WifiStatus wifi_status = 2;
|
||||
LoadSwStatus sw_status = 3;
|
||||
UartData uart_data = 4;
|
||||
}
|
||||
}
|
||||
BIN
schematic.pdf
Normal file
BIN
schematic.pdf
Normal file
Binary file not shown.
@@ -7,17 +7,23 @@ CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_UART_ISR_IN_IRAM=y
|
||||
CONFIG_HTTPD_MAX_REQ_HDR_LEN=2048
|
||||
CONFIG_HTTPD_MAX_URI_LEN=1024
|
||||
CONFIG_HTTPD_WS_SUPPORT=y
|
||||
CONFIG_ESP_HTTPS_SERVER_ENABLE=y
|
||||
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
|
||||
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=20
|
||||
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=40
|
||||
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=40
|
||||
CONFIG_ESP_WIFI_TX_BA_WIN=32
|
||||
CONFIG_ESP_WIFI_RX_BA_WIN=32
|
||||
CONFIG_FREERTOS_HZ=500
|
||||
CONFIG_LWIP_LOCAL_HOSTNAME="odroid-pm"
|
||||
CONFIG_LWIP_TCPIP_CORE_LOCKING=y
|
||||
CONFIG_LWIP_TCPIP_CORE_LOCKING_INPUT=y
|
||||
CONFIG_LWIP_IRAM_OPTIMIZATION=y
|
||||
CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=64
|
||||
CONFIG_LWIP_IPV6=n
|
||||
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=65534
|
||||
CONFIG_LWIP_TCP_WND_DEFAULT=65534
|
||||
CONFIG_LWIP_TCP_RECVMBOX_SIZE=64
|
||||
|
||||
Reference in New Issue
Block a user