mirror of
https://github.com/ARM-software/devlib.git
synced 2025-01-31 02:00:45 +00:00
devlib initial commit.
This commit is contained in:
commit
4e6afe960b
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.orig
|
||||||
|
.ropeproject
|
||||||
|
*.egg-info
|
37
README.rst
Normal file
37
README.rst
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
devlib
|
||||||
|
======
|
||||||
|
|
||||||
|
``devlib`` exposes an interface for interacting with and collecting
|
||||||
|
measurements from a variety of devices (such as mobile phones, tablets and
|
||||||
|
development boards) running a Linux-based operating system.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
sudo -H pip install devlib
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Please refer to the "Overview" section of the documentation.
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
This package is distributed under `Apache v2.0 License <http://www.apache.org/licenses/LICENSE-2.0>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Feedback, Contrubutions and Support
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
- Please use the GitHub Issue Tracker associated with this repository for
|
||||||
|
feedback.
|
||||||
|
- ARM licensees may contact ARM directly via their partner managers.
|
||||||
|
- We welcome code contributions via GitHub Pull requests. Please try to
|
||||||
|
stick to the style in the rest of the code for your contributions.
|
||||||
|
|
18
devlib/__init__.py
Normal file
18
devlib/__init__.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from devlib.target import Target, LinuxTarget, AndroidTarget, LocalLinuxTarget
|
||||||
|
from devlib.host import PACKAGE_BIN_DIRECTORY
|
||||||
|
from devlib.exception import DevlibError, TargetError, HostError, TargetNotRespondingError
|
||||||
|
|
||||||
|
from devlib.module import Module, HardRestModule, BootModule, FlashModule
|
||||||
|
from devlib.module import get_module, register_module
|
||||||
|
|
||||||
|
from devlib.platform import Platform
|
||||||
|
from devlib.platform.arm import TC2, Juno, JunoEnergyInstrument
|
||||||
|
|
||||||
|
from devlib.instrument import Instrument, InstrumentChannel, Measurement, MeasurementsCsv
|
||||||
|
from devlib.instrument import MEASUREMENT_TYPES, INSTANTANEOUS, CONTINUOUS
|
||||||
|
from devlib.instrument.daq import DaqInstrument
|
||||||
|
from devlib.instrument.energy_probe import EnergyProbeInstrument
|
||||||
|
from devlib.instrument.hwmon import HwmonInstrument
|
||||||
|
from devlib.instrument.netstats import NetstatsInstrument
|
||||||
|
|
||||||
|
from devlib.trace.ftrace import FtraceCollector
|
348
devlib/bin/LICENSE.busybox
Normal file
348
devlib/bin/LICENSE.busybox
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
--- A note on GPL versions
|
||||||
|
|
||||||
|
BusyBox is distributed under version 2 of the General Public License (included
|
||||||
|
in its entirety, below). Version 2 is the only version of this license which
|
||||||
|
this version of BusyBox (or modified versions derived from this one) may be
|
||||||
|
distributed under.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Library General Public License instead.) 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
|
||||||
|
this service 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 make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. 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.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
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
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the 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 a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
convey 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 2 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, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision 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, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This 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 Library General
|
||||||
|
Public License instead of this License.
|
39
devlib/bin/LICENSE.trace-cmd
Normal file
39
devlib/bin/LICENSE.trace-cmd
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
Included trace-cmd binaries are Free Software ditributed under GPLv2:
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2009, 2010 Red Hat Inc, Steven Rostedt <srostedt@redhat.com>
|
||||||
|
*
|
||||||
|
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
*
|
||||||
|
* 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; version 2 of the License (not later!)
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses>
|
||||||
|
*
|
||||||
|
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
*/
|
||||||
|
|
||||||
|
The full text of the license may be viewed here:
|
||||||
|
|
||||||
|
http://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
|
||||||
|
Source code for trace-cmd may be obtained here:
|
||||||
|
|
||||||
|
git://git.kernel.org/pub/scm/linux/kernel/git/rostedt/trace-cmd.git
|
||||||
|
|
||||||
|
Binaries included here contain modifications by ARM that, at the time of writing,
|
||||||
|
have not yet made it into the above repository. The patches for these modifications
|
||||||
|
are available here:
|
||||||
|
|
||||||
|
http://article.gmane.org/gmane.linux.kernel/1869111
|
||||||
|
http://article.gmane.org/gmane.linux.kernel/1869112
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
devlib/bin/arm64/busybox
Executable file
BIN
devlib/bin/arm64/busybox
Executable file
Binary file not shown.
BIN
devlib/bin/arm64/readenergy
Executable file
BIN
devlib/bin/arm64/readenergy
Executable file
Binary file not shown.
BIN
devlib/bin/arm64/trace-cmd
Executable file
BIN
devlib/bin/arm64/trace-cmd
Executable file
Binary file not shown.
BIN
devlib/bin/armeabi/busybox
Executable file
BIN
devlib/bin/armeabi/busybox
Executable file
Binary file not shown.
BIN
devlib/bin/armeabi/trace-cmd
Executable file
BIN
devlib/bin/armeabi/trace-cmd
Executable file
Binary file not shown.
40
devlib/exception.py
Normal file
40
devlib/exception.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Copyright 2013-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
from devlib.utils.misc import TimeoutError # NOQA pylint: disable=W0611
|
||||||
|
|
||||||
|
|
||||||
|
class DevlibError(Exception):
|
||||||
|
"""Base class for all Workload Automation exceptions."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TargetError(DevlibError):
|
||||||
|
"""An error has occured on the target"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TargetNotRespondingError(DevlibError):
|
||||||
|
"""The target is unresponsive."""
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
super(TargetNotRespondingError, self).__init__('Target {} is not responding.'.format(target))
|
||||||
|
|
||||||
|
|
||||||
|
class HostError(DevlibError):
|
||||||
|
"""An error has occured on the host"""
|
||||||
|
pass
|
||||||
|
|
80
devlib/host.py
Normal file
80
devlib/host.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Copyright 2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from getpass import getpass
|
||||||
|
|
||||||
|
from devlib.exception import TargetError
|
||||||
|
from devlib.utils.misc import check_output
|
||||||
|
|
||||||
|
PACKAGE_BIN_DIRECTORY = os.path.join(os.path.dirname(__file__), 'bin')
|
||||||
|
|
||||||
|
|
||||||
|
class LocalConnection(object):
|
||||||
|
|
||||||
|
name = 'local'
|
||||||
|
|
||||||
|
def __init__(self, timeout=10, keep_password=True, unrooted=False):
|
||||||
|
self.logger = logging.getLogger('local_connection')
|
||||||
|
self.timeout = timeout
|
||||||
|
self.keep_password = keep_password
|
||||||
|
self.unrooted = unrooted
|
||||||
|
self.password = None
|
||||||
|
|
||||||
|
def push(self, source, dest, timeout=None, as_root=False): # pylint: disable=unused-argument
|
||||||
|
self.logger.debug('cp {} {}'.format(source, dest))
|
||||||
|
shutil.copy(source, dest)
|
||||||
|
|
||||||
|
def pull(self, source, dest, timeout=None, as_root=False): # pylint: disable=unused-argument
|
||||||
|
self.logger.debug('cp {} {}'.format(source, dest))
|
||||||
|
shutil.copy(source, dest)
|
||||||
|
|
||||||
|
def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
|
||||||
|
self.logger.debug(command)
|
||||||
|
if as_root:
|
||||||
|
if self.unrooted:
|
||||||
|
raise TargetError('unrooted')
|
||||||
|
password = self._get_password()
|
||||||
|
command = 'echo \'{}\' | sudo -S '.format(password) + command
|
||||||
|
ignore = None if check_exit_code else 'all'
|
||||||
|
try:
|
||||||
|
return check_output(command, shell=True, timeout=timeout, ignore=ignore)[0]
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
raise TargetError(e)
|
||||||
|
|
||||||
|
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
|
||||||
|
if as_root:
|
||||||
|
if self.unrooted:
|
||||||
|
raise TargetError('unrooted')
|
||||||
|
password = self._get_password()
|
||||||
|
command = 'echo \'{}\' | sudo -S '.format(password) + command
|
||||||
|
return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cancel_running_command(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_password(self):
|
||||||
|
if self.password:
|
||||||
|
return self.password
|
||||||
|
password = getpass('sudo password:')
|
||||||
|
if self.keep_password:
|
||||||
|
self.password = password
|
||||||
|
return password
|
||||||
|
|
225
devlib/instrument/__init__.py
Normal file
225
devlib/instrument/__init__.py
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
# Copyright 2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
import csv
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from devlib.utils.types import numeric
|
||||||
|
|
||||||
|
|
||||||
|
# Channel modes describe what sort of measurement the instrument supports.
|
||||||
|
# Values must be powers of 2
|
||||||
|
INSTANTANEOUS = 1
|
||||||
|
CONTINUOUS = 2
|
||||||
|
|
||||||
|
|
||||||
|
class MeasurementType(tuple):
|
||||||
|
|
||||||
|
__slots__ = []
|
||||||
|
|
||||||
|
def __new__(cls, name, units, category=None):
|
||||||
|
return tuple.__new__(cls, (name, units, category))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return tuple.__getitem__(self, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def units(self):
|
||||||
|
return tuple.__getitem__(self, 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category(self):
|
||||||
|
return tuple.__getitem__(self, 2)
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
raise TypeError()
|
||||||
|
|
||||||
|
def __cmp__(self, other):
|
||||||
|
if isinstance(other, MeasurementType):
|
||||||
|
other = other.name
|
||||||
|
return cmp(self.name, other)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
|
||||||
|
# Standard measures
|
||||||
|
_measurement_types = [
|
||||||
|
MeasurementType('time', 'seconds'),
|
||||||
|
MeasurementType('temperature', 'degrees'),
|
||||||
|
|
||||||
|
MeasurementType('power', 'watts', 'power/energy'),
|
||||||
|
MeasurementType('voltage', 'volts', 'power/energy'),
|
||||||
|
MeasurementType('current', 'amps', 'power/energy'),
|
||||||
|
MeasurementType('energy', 'joules', 'power/energy'),
|
||||||
|
|
||||||
|
MeasurementType('tx', 'bytes', 'data transfer'),
|
||||||
|
MeasurementType('rx', 'bytes', 'data transfer'),
|
||||||
|
MeasurementType('tx/rx', 'bytes', 'data transfer'),
|
||||||
|
]
|
||||||
|
MEASUREMENT_TYPES = {m.name: m for m in _measurement_types}
|
||||||
|
|
||||||
|
|
||||||
|
class Measurement(object):
|
||||||
|
|
||||||
|
__slots__ = ['value', 'channel']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return '{}_{}'.format(self.channel.site, self.channel.kind)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def units(self):
|
||||||
|
return self.channel.units
|
||||||
|
|
||||||
|
def __init__(self, value, channel):
|
||||||
|
self.value = value
|
||||||
|
self.channel = channel
|
||||||
|
|
||||||
|
def __cmp__(self, other):
|
||||||
|
if isinstance(other, Measurement):
|
||||||
|
return cmp(self.value, other.value)
|
||||||
|
else:
|
||||||
|
return cmp(self.value, other)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.units:
|
||||||
|
return '{}: {} {}'.format(self.name, self.value, self.units)
|
||||||
|
else:
|
||||||
|
return '{}: {}'.format(self.name, self.value)
|
||||||
|
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
|
||||||
|
class MeasurementsCsv(object):
|
||||||
|
|
||||||
|
def __init__(self, path, channels):
|
||||||
|
self.path = path
|
||||||
|
self.channels = channels
|
||||||
|
self._fh = open(path, 'rb')
|
||||||
|
|
||||||
|
def measurements(self):
|
||||||
|
return list(self.itermeasurements())
|
||||||
|
|
||||||
|
def itermeasurements(self):
|
||||||
|
self._fh.seek(0)
|
||||||
|
reader = csv.reader(self._fh)
|
||||||
|
reader.next() # headings
|
||||||
|
for row in reader:
|
||||||
|
values = map(numeric, row)
|
||||||
|
yield [Measurement(v, c) for (v, c) in zip(values, self.channels)]
|
||||||
|
|
||||||
|
|
||||||
|
class InstrumentChannel(object):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self):
|
||||||
|
return '{}_{}'.format(self.site, self.kind)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kind(self):
|
||||||
|
return self.measurement_type.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def units(self):
|
||||||
|
return self.measurement_type.units
|
||||||
|
|
||||||
|
def __init__(self, name, site, measurement_type, **attrs):
|
||||||
|
self.name = name
|
||||||
|
self.site = site
|
||||||
|
if isinstance(measurement_type, MeasurementType):
|
||||||
|
self.measurement_type = measurement_type
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.measurement_type = MEASUREMENT_TYPES[measurement_type]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError('Unknown measurement type: {}'.format(measurement_type))
|
||||||
|
for atname, atvalue in attrs.iteritems():
|
||||||
|
setattr(self, atname, atvalue)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.name == self.label:
|
||||||
|
return 'CHAN({})'.format(self.label)
|
||||||
|
else:
|
||||||
|
return 'CHAN({}, {})'.format(self.name, self.label)
|
||||||
|
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
|
||||||
|
class Instrument(object):
|
||||||
|
|
||||||
|
mode = 0
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
self.target = target
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
self.channels = {}
|
||||||
|
self.active_channels = []
|
||||||
|
|
||||||
|
# channel management
|
||||||
|
|
||||||
|
def list_channels(self):
|
||||||
|
return self.channels.values()
|
||||||
|
|
||||||
|
def get_channels(self, measure):
|
||||||
|
if hasattr(measure, 'name'):
|
||||||
|
measure = measure.name
|
||||||
|
return [c for c in self.channels if c.measure.name == measure]
|
||||||
|
|
||||||
|
def add_channel(self, site, measure, name=None, **attrs):
|
||||||
|
if name is None:
|
||||||
|
name = '{}_{}'.format(site, measure)
|
||||||
|
chan = InstrumentChannel(name, site, measure, **attrs)
|
||||||
|
self.channels[chan.label] = chan
|
||||||
|
|
||||||
|
# initialization and teardown
|
||||||
|
|
||||||
|
def setup(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset(self, sites=None, kinds=None):
|
||||||
|
if kinds is None and sites is None:
|
||||||
|
self.active_channels = sorted(self.channels.values(), key=lambda x: x.label)
|
||||||
|
else:
|
||||||
|
if isinstance(sites, basestring):
|
||||||
|
sites = [sites]
|
||||||
|
if isinstance(kinds, basestring):
|
||||||
|
kinds = [kinds]
|
||||||
|
self.active_channels = []
|
||||||
|
for chan in self.channels.values():
|
||||||
|
if (kinds is None or chan.kind in kinds) and \
|
||||||
|
(sites is None or chan.site in sites):
|
||||||
|
self.active_channels.append(chan)
|
||||||
|
|
||||||
|
# instantaneous
|
||||||
|
|
||||||
|
def take_measurement(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# continuous
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_data(self, outfile):
|
||||||
|
pass
|
138
devlib/instrument/daq.py
Normal file
138
devlib/instrument/daq.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import os
|
||||||
|
import csv
|
||||||
|
import tempfile
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from devlib.instrument import Instrument, MeasurementsCsv, CONTINUOUS
|
||||||
|
from devlib.exception import HostError
|
||||||
|
from devlib.utils.misc import unique
|
||||||
|
|
||||||
|
try:
|
||||||
|
from daqpower.client import execute_command, Status
|
||||||
|
from daqpower.config import DeviceConfiguration, ServerConfiguration
|
||||||
|
except ImportError, e:
|
||||||
|
execute_command, Status = None, None
|
||||||
|
DeviceConfiguration, ServerConfiguration, ConfigurationError = None, None, None
|
||||||
|
import_error_mesg = e.message
|
||||||
|
|
||||||
|
|
||||||
|
class DaqInstrument(Instrument):
|
||||||
|
|
||||||
|
mode = CONTINUOUS
|
||||||
|
|
||||||
|
def __init__(self, target, resistor_values, # pylint: disable=R0914
|
||||||
|
labels=None,
|
||||||
|
host='localhost',
|
||||||
|
port=45677,
|
||||||
|
device_id='Dev1',
|
||||||
|
v_range=2.5,
|
||||||
|
dv_range=0.2,
|
||||||
|
sampling_rate=10000,
|
||||||
|
channel_map=(0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23),
|
||||||
|
):
|
||||||
|
# pylint: disable=no-member
|
||||||
|
super(DaqInstrument, self).__init__(target)
|
||||||
|
self._need_reset = True
|
||||||
|
if execute_command is None:
|
||||||
|
raise HostError('Could not import "daqpower": {}'.format(import_error_mesg))
|
||||||
|
if labels is None:
|
||||||
|
labels = ['PORT_{}'.format(i) for i in xrange(len(resistor_values))]
|
||||||
|
if len(labels) != len(resistor_values):
|
||||||
|
raise ValueError('"labels" and "resistor_values" must be of the same length')
|
||||||
|
self.server_config = ServerConfiguration(host=host,
|
||||||
|
port=port)
|
||||||
|
result = self.execute('list_devices')
|
||||||
|
if result.status == Status.OK:
|
||||||
|
if device_id not in result.data:
|
||||||
|
raise ValueError('Device "{}" is not found on the DAQ server.'.format(device_id))
|
||||||
|
elif result.status != Status.OKISH:
|
||||||
|
raise HostError('Problem querying DAQ server: {}'.format(result.message))
|
||||||
|
|
||||||
|
self.device_config = DeviceConfiguration(device_id=device_id,
|
||||||
|
v_range=v_range,
|
||||||
|
dv_range=dv_range,
|
||||||
|
sampling_rate=sampling_rate,
|
||||||
|
resistor_values=resistor_values,
|
||||||
|
channel_map=channel_map,
|
||||||
|
labels=labels)
|
||||||
|
|
||||||
|
for label in labels:
|
||||||
|
for kind in ['power', 'voltage']:
|
||||||
|
self.add_channel(label, kind)
|
||||||
|
|
||||||
|
def reset(self, sites=None, kinds=None):
|
||||||
|
super(DaqInstrument, self).reset(sites, kinds)
|
||||||
|
self.execute('close')
|
||||||
|
result = self.execute('configure', config=self.device_config)
|
||||||
|
if not result.status == Status.OK: # pylint: disable=no-member
|
||||||
|
raise HostError(result.message)
|
||||||
|
self._need_reset = False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self._need_reset:
|
||||||
|
self.reset()
|
||||||
|
self.execute('start')
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.execute('stop')
|
||||||
|
self._need_reset = True
|
||||||
|
|
||||||
|
def get_data(self, outfile): # pylint: disable=R0914
|
||||||
|
tempdir = tempfile.mkdtemp(prefix='daq-raw-')
|
||||||
|
self.execute('get_data', output_directory=tempdir)
|
||||||
|
raw_file_map = {}
|
||||||
|
for entry in os.listdir(tempdir):
|
||||||
|
site = os.path.splitext(entry)[0]
|
||||||
|
path = os.path.join(tempdir, entry)
|
||||||
|
raw_file_map[site] = path
|
||||||
|
|
||||||
|
active_sites = unique([c.site for c in self.active_channels])
|
||||||
|
file_handles = []
|
||||||
|
try:
|
||||||
|
site_readers = {}
|
||||||
|
for site in active_sites:
|
||||||
|
try:
|
||||||
|
site_file = raw_file_map[site]
|
||||||
|
fh = open(site_file, 'rb')
|
||||||
|
site_readers[site] = csv.reader(fh)
|
||||||
|
file_handles.append(fh)
|
||||||
|
except KeyError:
|
||||||
|
message = 'Could not get DAQ trace for {}; Obtained traces are in {}'
|
||||||
|
raise HostError(message.format(site, tempdir))
|
||||||
|
|
||||||
|
# The first row is the headers
|
||||||
|
channel_order = []
|
||||||
|
for site, reader in site_readers.iteritems():
|
||||||
|
channel_order.extend(['{}_{}'.format(site, kind)
|
||||||
|
for kind in reader.next()])
|
||||||
|
|
||||||
|
def _read_next_rows():
|
||||||
|
parts = []
|
||||||
|
for reader in site_readers.itervalues():
|
||||||
|
try:
|
||||||
|
parts.extend(reader.next())
|
||||||
|
except StopIteration:
|
||||||
|
parts.extend([None, None])
|
||||||
|
return list(chain(parts))
|
||||||
|
|
||||||
|
with open(outfile, 'wb') as wfh:
|
||||||
|
field_names = [c.label for c in self.active_channels]
|
||||||
|
writer = csv.writer(wfh)
|
||||||
|
writer.writerow(field_names)
|
||||||
|
raw_row = _read_next_rows()
|
||||||
|
while any(raw_row):
|
||||||
|
row = [raw_row[channel_order.index(f)] for f in field_names]
|
||||||
|
writer.writerow(row)
|
||||||
|
raw_row = _read_next_rows()
|
||||||
|
|
||||||
|
return MeasurementsCsv(outfile, self.active_channels)
|
||||||
|
finally:
|
||||||
|
for fh in file_handles:
|
||||||
|
fh.close()
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
|
self.execute('close')
|
||||||
|
|
||||||
|
def execute(self, command, **kwargs):
|
||||||
|
return execute_command(self.server_config, command, **kwargs)
|
||||||
|
|
116
devlib/instrument/energy_probe.py
Normal file
116
devlib/instrument/energy_probe.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# Copyright 2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
from __future__ import division
|
||||||
|
import os
|
||||||
|
import csv
|
||||||
|
import signal
|
||||||
|
import tempfile
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pandas
|
||||||
|
except ImportError:
|
||||||
|
pandas = None
|
||||||
|
|
||||||
|
from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
|
||||||
|
from devlib.exception import HostError
|
||||||
|
from devlib.utils.misc import which
|
||||||
|
|
||||||
|
|
||||||
|
class EnergyProbeInstrument(Instrument):
|
||||||
|
|
||||||
|
mode = CONTINUOUS
|
||||||
|
|
||||||
|
def __init__(self, target, resistor_values,
|
||||||
|
labels=None,
|
||||||
|
device_entry='/dev/ttyACM0',
|
||||||
|
):
|
||||||
|
super(EnergyProbeInstrument, self).__init__(target)
|
||||||
|
self.resistor_values = resistor_values
|
||||||
|
if labels is not None:
|
||||||
|
self.labels = labels
|
||||||
|
else:
|
||||||
|
self.labels = ['PORT_{}'.format(i)
|
||||||
|
for i in xrange(len(resistor_values))]
|
||||||
|
self.device_entry = device_entry
|
||||||
|
self.caiman = which('caiman')
|
||||||
|
if self.caiman is None:
|
||||||
|
raise HostError('caiman must be installed on the host '
|
||||||
|
'(see https://github.com/ARM-software/caiman)')
|
||||||
|
if pandas is None:
|
||||||
|
self.logger.info("pandas package will significantly speed up this instrument")
|
||||||
|
self.logger.info("to install it try: pip install pandas")
|
||||||
|
self.attributes_per_sample = 3
|
||||||
|
self.bytes_per_sample = self.attributes_per_sample * 4
|
||||||
|
self.attributes = ['power', 'voltage', 'current']
|
||||||
|
self.command = None
|
||||||
|
self.raw_output_directory = None
|
||||||
|
self.process = None
|
||||||
|
|
||||||
|
for label in self.labels:
|
||||||
|
for kind in self.attributes:
|
||||||
|
self.add_channel(label, kind)
|
||||||
|
|
||||||
|
def reset(self, sites=None, kinds=None):
|
||||||
|
super(EnergyProbeInstrument, self).reset(sites, kinds)
|
||||||
|
self.raw_output_directory = tempfile.mkdtemp(prefix='eprobe-caiman-')
|
||||||
|
parts = ['-r {}:{} '.format(i, int(1000 * rval))
|
||||||
|
for i, rval in enumerate(self.resistor_values)]
|
||||||
|
rstring = ''.join(parts)
|
||||||
|
self.command = '{} -d {} -l {} {}'.format(self.caiman, self.device_entry, rstring, self.raw_output_directory)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.logger.debug(self.command)
|
||||||
|
self.process = subprocess.Popen(self.command,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
preexec_fn=os.setpgrp,
|
||||||
|
shell=True)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
os.killpg(self.process.pid, signal.SIGTERM)
|
||||||
|
|
||||||
|
def get_data(self, outfile): # pylint: disable=R0914
|
||||||
|
all_channels = [c.label for c in self.list_channels()]
|
||||||
|
active_channels = [c.label for c in self.active_channels]
|
||||||
|
active_indexes = [all_channels.index(ac) for ac in active_channels]
|
||||||
|
|
||||||
|
num_of_ports = len(self.resistor_values)
|
||||||
|
struct_format = '{}I'.format(num_of_ports * self.attributes_per_sample)
|
||||||
|
not_a_full_row_seen = False
|
||||||
|
raw_data_file = os.path.join(self.raw_output_directory, '0000000000')
|
||||||
|
|
||||||
|
self.logger.debug('Parsing raw data file: {}'.format(raw_data_file))
|
||||||
|
with open(raw_data_file, 'rb') as bfile:
|
||||||
|
with open(outfile, 'wb') as wfh:
|
||||||
|
writer = csv.writer(wfh)
|
||||||
|
writer.writerow(active_channels)
|
||||||
|
while True:
|
||||||
|
data = bfile.read(num_of_ports * self.bytes_per_sample)
|
||||||
|
if data == '':
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
unpacked_data = struct.unpack(struct_format, data)
|
||||||
|
row = [unpacked_data[i] / 1000 for i in active_indexes]
|
||||||
|
writer.writerow(row)
|
||||||
|
except struct.error:
|
||||||
|
if not_a_full_row_seen:
|
||||||
|
self.logger.warn('possibly missaligned caiman raw data, row contained {} bytes'.format(len(data)))
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
not_a_full_row_seen = True
|
||||||
|
return MeasurementsCsv(outfile, self.active_channels)
|
89
devlib/instrument/hwmon.py
Normal file
89
devlib/instrument/hwmon.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# Copyright 2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
from __future__ import division
|
||||||
|
import re
|
||||||
|
|
||||||
|
from devlib.instrument import Instrument, Measurement, INSTANTANEOUS
|
||||||
|
from devlib.exception import TargetError
|
||||||
|
|
||||||
|
|
||||||
|
class HwmonInstrument(Instrument):
|
||||||
|
|
||||||
|
name = 'hwmon'
|
||||||
|
mode = INSTANTANEOUS
|
||||||
|
|
||||||
|
# sensor kind --> (meaure, standard unit conversion)
|
||||||
|
measure_map = {
|
||||||
|
'temp': ('temperature', lambda x: x / 1000),
|
||||||
|
'in': ('voltage', lambda x: x / 1000),
|
||||||
|
'curr': ('current', lambda x: x / 1000),
|
||||||
|
'power': ('power', lambda x: x / 1000000),
|
||||||
|
'energy': ('energy', lambda x: x / 1000000),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
if not hasattr(target, 'hwmon'):
|
||||||
|
raise TargetError('Target does not support HWMON')
|
||||||
|
super(HwmonInstrument, self).__init__(target)
|
||||||
|
|
||||||
|
self.logger.debug('Discovering available HWMON sensors...')
|
||||||
|
for ts in self.target.hwmon.sensors:
|
||||||
|
try:
|
||||||
|
ts.get_file('input')
|
||||||
|
measure = self.measure_map.get(ts.kind)[0]
|
||||||
|
if measure:
|
||||||
|
self.logger.debug('\tAdding sensor {}'.format(ts.name))
|
||||||
|
self.add_channel(_guess_site(ts), measure, name=ts.name, sensor=ts)
|
||||||
|
else:
|
||||||
|
self.logger.debug('\tSkipping sensor {} (unknown kind "{}")'.format(ts.name, ts.kind))
|
||||||
|
except ValueError:
|
||||||
|
message = 'Skipping sensor {} because it does not have an input file'
|
||||||
|
self.logger.debug(message.format(ts.name))
|
||||||
|
continue
|
||||||
|
|
||||||
|
def take_measurement(self):
|
||||||
|
result = []
|
||||||
|
for chan in self.active_channels:
|
||||||
|
convert = self.measure_map[chan.sensor.kind][1]
|
||||||
|
value = convert(chan.sensor.get('input'))
|
||||||
|
result.append(Measurement(value, chan))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _guess_site(sensor):
|
||||||
|
"""
|
||||||
|
HWMON does not specify a standard for labeling its sensors, or for
|
||||||
|
device/item split (the implication is that each hwmon device a separate chip
|
||||||
|
with possibly several sensors on it, but not everyone adheres to that, e.g.,
|
||||||
|
with some mobile devices splitting a chip's sensors across multiple hwmon
|
||||||
|
devices. This function processes name/label of the senors to attempt to
|
||||||
|
identify the best "candidate" for the site to which the sensor belongs.
|
||||||
|
"""
|
||||||
|
if sensor.name == sensor.label:
|
||||||
|
# If no label has been specified for the sensor (in which case, it
|
||||||
|
# defaults to the sensor's name), assume that the "site" of the sensor
|
||||||
|
# is identified by the HWMON device
|
||||||
|
text = sensor.device.name
|
||||||
|
else:
|
||||||
|
# If a label has been specified, assume multiple sensors controlled by
|
||||||
|
# the same device and the label identifies the site.
|
||||||
|
text = sensor.label
|
||||||
|
# strip out sensor kind suffix, if any, as that does not indicate a site
|
||||||
|
for kind in ['volt', 'in', 'curr', 'power', 'energy',
|
||||||
|
'temp', 'voltage', 'temperature', 'current']:
|
||||||
|
if kind in text.lower():
|
||||||
|
regex = re.compile(r'_*{}\d*_*'.format(kind), re.I)
|
||||||
|
text = regex.sub('', text)
|
||||||
|
return text.strip()
|
135
devlib/instrument/netstats/__init__.py
Normal file
135
devlib/instrument/netstats/__init__.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import csv
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime
|
||||||
|
from collections import defaultdict
|
||||||
|
from itertools import izip_longest
|
||||||
|
|
||||||
|
from devlib.instrument import Instrument, MeasurementsCsv, CONTINUOUS
|
||||||
|
from devlib.exception import TargetError, HostError
|
||||||
|
from devlib.utils.android import ApkInfo
|
||||||
|
|
||||||
|
|
||||||
|
THIS_DIR = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
NETSTAT_REGEX = re.compile(r'I/(?P<tag>netstats-\d+)\(\s*\d*\): (?P<ts>\d+) '
|
||||||
|
r'"(?P<package>[^"]+)" TX: (?P<tx>\S+) RX: (?P<rx>\S+)')
|
||||||
|
|
||||||
|
|
||||||
|
def extract_netstats(filepath, tag=None):
|
||||||
|
netstats = []
|
||||||
|
with open(filepath) as fh:
|
||||||
|
for line in fh:
|
||||||
|
match = NETSTAT_REGEX.search(line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
if tag and match.group('tag') != tag:
|
||||||
|
continue
|
||||||
|
netstats.append((match.group('tag'),
|
||||||
|
match.group('ts'),
|
||||||
|
match.group('package'),
|
||||||
|
match.group('tx'),
|
||||||
|
match.group('rx')))
|
||||||
|
return netstats
|
||||||
|
|
||||||
|
|
||||||
|
def netstats_to_measurements(netstats):
|
||||||
|
measurements = defaultdict(list)
|
||||||
|
for row in netstats:
|
||||||
|
tag, ts, package, tx, rx = row # pylint: disable=unused-variable
|
||||||
|
measurements[package + '_tx'].append(tx)
|
||||||
|
measurements[package + '_rx'].append(rx)
|
||||||
|
return measurements
|
||||||
|
|
||||||
|
|
||||||
|
def write_measurements_csv(measurements, filepath):
|
||||||
|
headers = sorted(measurements.keys())
|
||||||
|
columns = [measurements[h] for h in headers]
|
||||||
|
with open(filepath, 'wb') as wfh:
|
||||||
|
writer = csv.writer(wfh)
|
||||||
|
writer.writerow(headers)
|
||||||
|
writer.writerows(izip_longest(*columns))
|
||||||
|
|
||||||
|
|
||||||
|
class NetstatsInstrument(Instrument):
|
||||||
|
|
||||||
|
mode = CONTINUOUS
|
||||||
|
|
||||||
|
def __init__(self, target, apk=None, service='.TrafficMetricsService'):
|
||||||
|
"""
|
||||||
|
Additional paramerter:
|
||||||
|
|
||||||
|
:apk: Path to the APK file that contains ``com.arm.devlab.netstats``
|
||||||
|
package. If not specified, it will be assumed that an APK with
|
||||||
|
name "netstats.apk" is located in the same directory as the
|
||||||
|
Python module for the instrument.
|
||||||
|
:service: Name of the service to be launched. This service must be
|
||||||
|
present in the APK.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if target.os != 'android':
|
||||||
|
raise TargetError('netstats insturment only supports Android targets')
|
||||||
|
if apk is None:
|
||||||
|
apk = os.path.join(THIS_DIR, 'netstats.apk')
|
||||||
|
if not os.path.isfile(apk):
|
||||||
|
raise HostError('APK for netstats instrument does not exist ({})'.format(apk))
|
||||||
|
super(NetstatsInstrument, self).__init__(target)
|
||||||
|
self.apk = apk
|
||||||
|
self.package = ApkInfo(self.apk).package
|
||||||
|
self.service = service
|
||||||
|
self.tag = None
|
||||||
|
self.command = None
|
||||||
|
self.stop_command = 'am kill {}'.format(self.package)
|
||||||
|
|
||||||
|
for package in self.target.list_packages():
|
||||||
|
self.add_channel(package, 'tx')
|
||||||
|
self.add_channel(package, 'rx')
|
||||||
|
|
||||||
|
def setup(self, force=False, *args, **kwargs):
|
||||||
|
if self.target.package_is_installed(self.package):
|
||||||
|
if force:
|
||||||
|
self.logger.debug('Re-installing {} (forced)'.format(self.package))
|
||||||
|
self.target.uninstall_package(self.package)
|
||||||
|
self.target.install(self.apk)
|
||||||
|
else:
|
||||||
|
self.logger.debug('{} already present on target'.format(self.package))
|
||||||
|
else:
|
||||||
|
self.logger.debug('Deploying {} to target'.format(self.package))
|
||||||
|
self.target.install(self.apk)
|
||||||
|
|
||||||
|
def reset(self, sites=None, kinds=None, period=None): # pylint: disable=arguments-differ
|
||||||
|
super(NetstatsInstrument, self).reset(sites, kinds)
|
||||||
|
period_arg, packages_arg = '', ''
|
||||||
|
self.tag = 'netstats-{}'.format(datetime.now().strftime('%Y%m%d%H%M%s'))
|
||||||
|
tag_arg = ' --es tag {}'.format(self.tag)
|
||||||
|
if sites:
|
||||||
|
packages_arg = ' --es packages {}'.format(','.join(sites))
|
||||||
|
if period:
|
||||||
|
period_arg = ' --ei period {}'.format(period)
|
||||||
|
self.command = 'am startservice{}{}{} {}/{}'.format(tag_arg,
|
||||||
|
period_arg,
|
||||||
|
packages_arg,
|
||||||
|
self.package,
|
||||||
|
self.service)
|
||||||
|
self.target.execute(self.stop_command) # ensure the service is not running.
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self.command is None:
|
||||||
|
raise RuntimeError('reset() must be called before start()')
|
||||||
|
self.target.execute(self.command)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.target.execute(self.stop_command)
|
||||||
|
|
||||||
|
def get_data(self, outfile):
|
||||||
|
raw_log_file = tempfile.mktemp()
|
||||||
|
self.target.dump_logcat(raw_log_file)
|
||||||
|
data = extract_netstats(raw_log_file)
|
||||||
|
measurements = netstats_to_measurements(data)
|
||||||
|
write_measurements_csv(measurements, outfile)
|
||||||
|
os.remove(raw_log_file)
|
||||||
|
return MeasurementsCsv(outfile, self.active_channels)
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
|
self.target.uninstall_package(self.package)
|
BIN
devlib/instrument/netstats/netstats.apk
Normal file
BIN
devlib/instrument/netstats/netstats.apk
Normal file
Binary file not shown.
122
devlib/module/__init__.py
Normal file
122
devlib/module/__init__.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
# Copyright 2014-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
from inspect import isclass
|
||||||
|
|
||||||
|
from devlib.utils.misc import walk_modules
|
||||||
|
from devlib.utils.types import identifier
|
||||||
|
|
||||||
|
|
||||||
|
__module_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Module(object):
|
||||||
|
|
||||||
|
name = None
|
||||||
|
kind = None
|
||||||
|
# This is the stage at which the module will be installed. Current valid
|
||||||
|
# stages are:
|
||||||
|
# 'early' -- installed when the Target is first created. This should be
|
||||||
|
# used for modules that do not rely on the main connection
|
||||||
|
# being established (usually because the commumnitcate with the
|
||||||
|
# target through some sorto of secondary connection, e.g. via
|
||||||
|
# serial).
|
||||||
|
# 'connected' -- installed when a connection to to the target has been
|
||||||
|
# established. This is the default.
|
||||||
|
stage = 'connected'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def install(cls, target, **params):
|
||||||
|
if cls.kind is not None:
|
||||||
|
attr_name = identifier(cls.kind)
|
||||||
|
else:
|
||||||
|
attr_name = identifier(cls.name)
|
||||||
|
if hasattr(target, attr_name):
|
||||||
|
existing_module = getattr(target, attr_name)
|
||||||
|
existing_name = getattr(existing_module, 'name', str(existing_module))
|
||||||
|
message = 'Attempting to install module "{}" which already exists (new: {}, existing: {})'
|
||||||
|
raise ValueError(message.format(attr_name, cls.name, existing_name))
|
||||||
|
setattr(target, attr_name, cls(target, **params))
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
self.target = target
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HardRestModule(Module): # pylint: disable=R0921
|
||||||
|
|
||||||
|
kind = 'hard_reset'
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class BootModule(Module): # pylint: disable=R0921
|
||||||
|
|
||||||
|
kind = 'boot'
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
for name, value in kwargs.iteritems():
|
||||||
|
if not hasattr(self, name):
|
||||||
|
raise ValueError('Unknown parameter "{}" for {}'.format(name, self.name))
|
||||||
|
self.logger.debug('Updating "{}" to "{}"'.format(name, value))
|
||||||
|
setattr(self, name, value)
|
||||||
|
|
||||||
|
|
||||||
|
class FlashModule(Module):
|
||||||
|
|
||||||
|
kind = 'flash'
|
||||||
|
|
||||||
|
def __call__(self, image_bundle=None, images=None, boot_config=None):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
def get_module(mod):
|
||||||
|
if not __module_cache:
|
||||||
|
__load_cache()
|
||||||
|
|
||||||
|
if isinstance(mod, basestring):
|
||||||
|
try:
|
||||||
|
return __module_cache[mod]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError('Module "{}" does not exist'.format(mod))
|
||||||
|
elif issubclass(mod, Module):
|
||||||
|
return mod
|
||||||
|
else:
|
||||||
|
raise ValueError('Not a valid module: {}'.format(mod))
|
||||||
|
|
||||||
|
|
||||||
|
def register_module(mod):
|
||||||
|
if not issubclass(mod, Module):
|
||||||
|
raise ValueError('A module must subclass devlib.Module')
|
||||||
|
if mod.name is None:
|
||||||
|
raise ValueError('A module must define a name')
|
||||||
|
if mod.name in __module_cache:
|
||||||
|
raise ValueError('Module {} already exists'.format(mod.name))
|
||||||
|
__module_cache[mod.name] = mod
|
||||||
|
|
||||||
|
|
||||||
|
def __load_cache():
|
||||||
|
for module in walk_modules('devlib.module'):
|
||||||
|
for obj in vars(module).itervalues():
|
||||||
|
if isclass(obj) and issubclass(obj, Module) and obj.name:
|
||||||
|
register_module(obj)
|
128
devlib/module/android.py
Normal file
128
devlib/module/android.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# Copyright 2014-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from devlib.module import FlashModule
|
||||||
|
from devlib.exception import HostError
|
||||||
|
from devlib.utils.android import fastboot_flash_partition, fastboot_command
|
||||||
|
from devlib.utils.misc import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
|
class FastbootFlashModule(FlashModule):
|
||||||
|
|
||||||
|
name = 'fastboot'
|
||||||
|
description = """
|
||||||
|
Enables automated flashing of images using the fastboot utility.
|
||||||
|
|
||||||
|
To use this flasher, a set of image files to be flused are required.
|
||||||
|
In addition a mapping between partitions and image file is required. There are two ways
|
||||||
|
to specify those requirements:
|
||||||
|
|
||||||
|
- Image mapping: In this mode, a mapping between partitions and images is given in the agenda.
|
||||||
|
- Image Bundle: In This mode a tarball is specified, which must contain all image files as well
|
||||||
|
as well as a partition file, named ``partitions.txt`` which contains the mapping between
|
||||||
|
partitions and images.
|
||||||
|
|
||||||
|
The format of ``partitions.txt`` defines one mapping per line as such: ::
|
||||||
|
|
||||||
|
kernel zImage-dtb
|
||||||
|
ramdisk ramdisk_image
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
delay = 0.5
|
||||||
|
partitions_file_name = 'partitions.txt'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
return target.os == 'android'
|
||||||
|
|
||||||
|
def __call__(self, image_bundle=None, images=None, bootargs=None):
|
||||||
|
if bootargs:
|
||||||
|
raise ValueError('{} does not support boot configuration'.format(self.name))
|
||||||
|
self.prelude_done = False
|
||||||
|
to_flash = {}
|
||||||
|
if image_bundle: # pylint: disable=access-member-before-definition
|
||||||
|
image_bundle = expand_path(image_bundle)
|
||||||
|
to_flash = self._bundle_to_images(image_bundle)
|
||||||
|
to_flash = merge_dicts(to_flash, images or {}, should_normalize=False)
|
||||||
|
for partition, image_path in to_flash.iteritems():
|
||||||
|
self.logger.debug('flashing {}'.format(partition))
|
||||||
|
self._flash_image(self.target, partition, expand_path(image_path))
|
||||||
|
fastboot_command('reboot')
|
||||||
|
self.target.connect(timeout=180)
|
||||||
|
|
||||||
|
def _validate_image_bundle(self, image_bundle):
|
||||||
|
if not tarfile.is_tarfile(image_bundle):
|
||||||
|
raise HostError('File {} is not a tarfile'.format(image_bundle))
|
||||||
|
with tarfile.open(image_bundle) as tar:
|
||||||
|
files = [tf.name for tf in tar.getmembers()]
|
||||||
|
if not any(pf in files for pf in (self.partitions_file_name, '{}/{}'.format(files[0], self.partitions_file_name))):
|
||||||
|
HostError('Image bundle does not contain the required partition file (see documentation)')
|
||||||
|
|
||||||
|
def _bundle_to_images(self, image_bundle):
|
||||||
|
"""
|
||||||
|
Extracts the bundle to a temporary location and creates a mapping between the contents of the bundle
|
||||||
|
and images to be flushed.
|
||||||
|
"""
|
||||||
|
self._validate_image_bundle(image_bundle)
|
||||||
|
extract_dir = tempfile.mkdtemp()
|
||||||
|
with tarfile.open(image_bundle) as tar:
|
||||||
|
tar.extractall(path=extract_dir)
|
||||||
|
files = [tf.name for tf in tar.getmembers()]
|
||||||
|
if self.partitions_file_name not in files:
|
||||||
|
extract_dir = os.path.join(extract_dir, files[0])
|
||||||
|
partition_file = os.path.join(extract_dir, self.partitions_file_name)
|
||||||
|
return get_mapping(extract_dir, partition_file)
|
||||||
|
|
||||||
|
def _flash_image(self, target, partition, image_path):
|
||||||
|
if not self.prelude_done:
|
||||||
|
self._fastboot_prelude(target)
|
||||||
|
fastboot_flash_partition(partition, image_path)
|
||||||
|
time.sleep(self.delay)
|
||||||
|
|
||||||
|
def _fastboot_prelude(self, target):
|
||||||
|
target.reset(fastboot=True)
|
||||||
|
time.sleep(self.delay)
|
||||||
|
self.prelude_done = True
|
||||||
|
|
||||||
|
|
||||||
|
# utility functions
|
||||||
|
|
||||||
|
def expand_path(original_path):
|
||||||
|
path = os.path.abspath(os.path.expanduser(original_path))
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise HostError('{} does not exist.'.format(path))
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def get_mapping(base_dir, partition_file):
|
||||||
|
mapping = {}
|
||||||
|
with open(partition_file) as pf:
|
||||||
|
for line in pf:
|
||||||
|
pair = line.split()
|
||||||
|
if len(pair) != 2:
|
||||||
|
HostError('partitions.txt is not properly formated')
|
||||||
|
image_path = os.path.join(base_dir, pair[1])
|
||||||
|
if not os.path.isfile(expand_path(image_path)):
|
||||||
|
HostError('file {} was not found in the bundle or was misplaced'.format(pair[1]))
|
||||||
|
mapping[pair[0]] = image_path
|
||||||
|
return mapping
|
||||||
|
|
122
devlib/module/biglittle.py
Normal file
122
devlib/module/biglittle.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
from devlib.module import Module
|
||||||
|
|
||||||
|
|
||||||
|
class BigLittleModule(Module):
|
||||||
|
|
||||||
|
name = 'bl'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
return target.big_core is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bigs(self):
|
||||||
|
return [i for i, c in enumerate(self.target.platform.core_names)
|
||||||
|
if c == self.target.platform.big_core]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def littles(self):
|
||||||
|
return [i for i, c in enumerate(self.target.platform.core_names)
|
||||||
|
if c == self.target.platform.little_core]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bigs_online(self):
|
||||||
|
return list(sorted(set(self.bigs).intersection(self.target.list_online_cpus())))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def littles_online(self):
|
||||||
|
return list(sorted(set(self.littles).intersection(self.target.list_online_cpus())))
|
||||||
|
|
||||||
|
# hotplug
|
||||||
|
|
||||||
|
def online_all_bigs(self):
|
||||||
|
self.target.hotplug.online(*self.bigs)
|
||||||
|
|
||||||
|
def offline_all_bigs(self):
|
||||||
|
self.target.hotplug.offline(*self.bigs)
|
||||||
|
|
||||||
|
def online_all_littles(self):
|
||||||
|
self.target.hotplug.online(*self.littles)
|
||||||
|
|
||||||
|
def offline_all_littles(self):
|
||||||
|
self.target.hotplug.offline(*self.littles)
|
||||||
|
|
||||||
|
# cpufreq
|
||||||
|
|
||||||
|
def list_bigs_frequencies(self):
|
||||||
|
return self.target.cpufreq.list_frequencies(self.bigs_online[0])
|
||||||
|
|
||||||
|
def list_bigs_governors(self):
|
||||||
|
return self.target.cpufreq.list_governors(self.bigs_online[0])
|
||||||
|
|
||||||
|
def list_bigs_governor_tunables(self):
|
||||||
|
return self.target.cpufreq.list_governor_tunables(self.bigs_online[0])
|
||||||
|
|
||||||
|
def list_littles_frequencies(self):
|
||||||
|
return self.target.cpufreq.list_frequencies(self.littles_online[0])
|
||||||
|
|
||||||
|
def list_littles_governors(self):
|
||||||
|
return self.target.cpufreq.list_governors(self.littles_online[0])
|
||||||
|
|
||||||
|
def list_littles_governor_tunables(self):
|
||||||
|
return self.target.cpufreq.list_governor_tunables(self.littles_online[0])
|
||||||
|
|
||||||
|
def get_bigs_governor(self):
|
||||||
|
return self.target.cpufreq.get_governor(self.bigs_online[0])
|
||||||
|
|
||||||
|
def get_bigs_governor_tunables(self):
|
||||||
|
return self.target.cpufreq.get_governor_tunables(self.bigs_online[0])
|
||||||
|
|
||||||
|
def get_bigs_frequency(self):
|
||||||
|
return self.target.cpufreq.get_frequency(self.bigs_online[0])
|
||||||
|
|
||||||
|
def get_bigs_min_frequency(self):
|
||||||
|
return self.target.cpufreq.get_min_frequency(self.bigs_online[0])
|
||||||
|
|
||||||
|
def get_bigs_max_frequency(self):
|
||||||
|
return self.target.cpufreq.get_max_frequency(self.bigs_online[0])
|
||||||
|
|
||||||
|
def get_littles_governor(self):
|
||||||
|
return self.target.cpufreq.get_governor(self.littles_online[0])
|
||||||
|
|
||||||
|
def get_littles_governor_tunables(self):
|
||||||
|
return self.target.cpufreq.get_governor_tunables(self.littles_online[0])
|
||||||
|
|
||||||
|
def get_littles_frequency(self):
|
||||||
|
return self.target.cpufreq.get_frequency(self.littles_online[0])
|
||||||
|
|
||||||
|
def get_littles_min_frequency(self):
|
||||||
|
return self.target.cpufreq.get_min_frequency(self.littles_online[0])
|
||||||
|
|
||||||
|
def get_littles_max_frequency(self):
|
||||||
|
return self.target.cpufreq.get_max_frequency(self.littles_online[0])
|
||||||
|
|
||||||
|
def set_bigs_governor(self, governor, **kwargs):
|
||||||
|
self.target.cpufreq.set_governor(self.bigs_online[0], governor, **kwargs)
|
||||||
|
|
||||||
|
def set_bigs_governor_tunables(self, governor, **kwargs):
|
||||||
|
self.target.cpufreq.set_governor_tunables(self.bigs_online[0], governor, **kwargs)
|
||||||
|
|
||||||
|
def set_bigs_frequency(self, frequency, exact=True):
|
||||||
|
self.target.cpufreq.set_frequency(self.bigs_online[0], frequency, exact)
|
||||||
|
|
||||||
|
def set_bigs_min_frequency(self, frequency, exact=True):
|
||||||
|
self.target.cpufreq.set_min_frequency(self.bigs_online[0], frequency, exact)
|
||||||
|
|
||||||
|
def set_bigs_max_frequency(self, frequency, exact=True):
|
||||||
|
self.target.cpufreq.set_max_frequency(self.bigs_online[0], frequency, exact)
|
||||||
|
|
||||||
|
def set_littles_governor(self, governor, **kwargs):
|
||||||
|
self.target.cpufreq.set_governor(self.littles_online[0], governor, **kwargs)
|
||||||
|
|
||||||
|
def set_littles_governor_tunables(self, governor, **kwargs):
|
||||||
|
self.target.cpufreq.set_governor_tunables(self.littles_online[0], governor, **kwargs)
|
||||||
|
|
||||||
|
def set_littles_frequency(self, frequency, exact=True):
|
||||||
|
self.target.cpufreq.set_frequency(self.littles_online[0], frequency, exact)
|
||||||
|
|
||||||
|
def set_littles_min_frequency(self, frequency, exact=True):
|
||||||
|
self.target.cpufreq.set_min_frequency(self.littles_online[0], frequency, exact)
|
||||||
|
|
||||||
|
def set_littles_max_frequency(self, frequency, exact=True):
|
||||||
|
self.target.cpufreq.set_max_frequency(self.littles_online[0], frequency, exact)
|
206
devlib/module/cgroups.py
Normal file
206
devlib/module/cgroups.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
# Copyright 2014-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
|
import logging
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from devlib.module import Module
|
||||||
|
from devlib.exception import TargetError
|
||||||
|
from devlib.utils.misc import list_to_ranges, isiterable
|
||||||
|
from devlib.utils.types import boolean
|
||||||
|
|
||||||
|
|
||||||
|
class CgroupController(object):
|
||||||
|
|
||||||
|
kind = 'cpuset'
|
||||||
|
|
||||||
|
def __new__(cls, arg):
|
||||||
|
if isinstance(arg, cls):
|
||||||
|
return arg
|
||||||
|
else:
|
||||||
|
return object.__new__(cls, arg)
|
||||||
|
|
||||||
|
def __init__(self, mount_name):
|
||||||
|
self.mount_point = None
|
||||||
|
self.mount_name = mount_name
|
||||||
|
self.logger = logging.getLogger(self.kind)
|
||||||
|
|
||||||
|
def probe(self, target):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def mount(self, device, mount_root):
|
||||||
|
self.target = device
|
||||||
|
self.mount_point = device.path.join(mount_root, self.mount_name)
|
||||||
|
mounted = self.target.list_file_systems()
|
||||||
|
if self.mount_point in [e.mount_point for e in mounted]:
|
||||||
|
self.logger.debug('controller is already mounted.')
|
||||||
|
else:
|
||||||
|
self.target.execute('mkdir -p {} 2>/dev/null'.format(self.mount_point),
|
||||||
|
as_root=True)
|
||||||
|
self.target.execute('mount -t cgroup -o {} {} {}'.format(self.kind,
|
||||||
|
self.mount_name,
|
||||||
|
self.mount_point),
|
||||||
|
as_root=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CpusetGroup(object):
|
||||||
|
|
||||||
|
def __init__(self, controller, name, cpus, mems):
|
||||||
|
self.controller = controller
|
||||||
|
self.target = controller.target
|
||||||
|
self.name = name
|
||||||
|
if name == 'root':
|
||||||
|
self.directory = controller.mount_point
|
||||||
|
else:
|
||||||
|
self.directory = self.target.path.join(controller.mount_point, name)
|
||||||
|
self.target.execute('mkdir -p {}'.format(self.directory), as_root=True)
|
||||||
|
self.cpus_file = self.target.path.join(self.directory, 'cpuset.cpus')
|
||||||
|
self.mems_file = self.target.path.join(self.directory, 'cpuset.mems')
|
||||||
|
self.tasks_file = self.target.path.join(self.directory, 'tasks')
|
||||||
|
self.set(cpus, mems)
|
||||||
|
|
||||||
|
def set(self, cpus, mems):
|
||||||
|
if isiterable(cpus):
|
||||||
|
cpus = list_to_ranges(cpus)
|
||||||
|
if isiterable(mems):
|
||||||
|
mems = list_to_ranges(mems)
|
||||||
|
self.target.write_value(self.cpus_file, cpus)
|
||||||
|
self.target.write_value(self.mems_file, mems)
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
cpus = self.target.read_value(self.cpus_file)
|
||||||
|
mems = self.target.read_value(self.mems_file)
|
||||||
|
return (cpus, mems)
|
||||||
|
|
||||||
|
def get_tasks(self):
|
||||||
|
task_ids = self.target.read_value(self.tasks_file).split()
|
||||||
|
return map(int, task_ids)
|
||||||
|
|
||||||
|
def add_tasks(self, tasks):
|
||||||
|
for tid in tasks:
|
||||||
|
self.add_task(tid)
|
||||||
|
|
||||||
|
def add_task(self, tid):
|
||||||
|
self.target.write_value(self.tasks_file, tid, verify=False)
|
||||||
|
|
||||||
|
|
||||||
|
class CpusetController(CgroupController):
|
||||||
|
|
||||||
|
name = 'cpuset'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CpusetController, self).__init__(*args, **kwargs)
|
||||||
|
self.groups = {}
|
||||||
|
|
||||||
|
def probe(self, target):
|
||||||
|
return target.config.is_enabled('cpusets')
|
||||||
|
|
||||||
|
def mount(self, device, mount_root):
|
||||||
|
super(CpusetController, self).mount(device, mount_root)
|
||||||
|
self.create_group('root', self.target.list_online_cpus(), 0)
|
||||||
|
|
||||||
|
def create_group(self, name, cpus, mems):
|
||||||
|
if not hasattr(self, 'target'):
|
||||||
|
raise RuntimeError('Attempting to create group for unmounted controller {}'.format(self.kind))
|
||||||
|
if name in self.groups:
|
||||||
|
raise ValueError('Group {} already exists'.format(name))
|
||||||
|
self.groups[name] = CpusetGroup(self, name, cpus, mems)
|
||||||
|
|
||||||
|
def move_tasks(self, source, dest):
|
||||||
|
try:
|
||||||
|
source_group = self.groups[source]
|
||||||
|
dest_group = self.groups[dest]
|
||||||
|
command = 'for task in $(cat {}); do echo $task>{}; done'
|
||||||
|
self.target.execute(command.format(source_group.tasks_file, dest_group.tasks_file),
|
||||||
|
# this will always fail as some of the tasks
|
||||||
|
# are kthreads that cannot be migrated, but we
|
||||||
|
# don't care about those, so don't check exit
|
||||||
|
# code.
|
||||||
|
check_exit_code=False, as_root=True)
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError('Unkown group: {}'.format(e))
|
||||||
|
|
||||||
|
def move_all_tasks_to(self, target_group):
|
||||||
|
for group in self.groups:
|
||||||
|
if group != target_group:
|
||||||
|
self.move_tasks(group, target_group)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
try:
|
||||||
|
return self.groups[name]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(name)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CgroupSubsystemEntry = namedtuple('CgroupSubsystemEntry', 'name hierarchy num_cgroups enabled')
|
||||||
|
|
||||||
|
|
||||||
|
class CgroupsModule(Module):
|
||||||
|
|
||||||
|
name = 'cgroups'
|
||||||
|
controller_cls = [
|
||||||
|
CpusetController,
|
||||||
|
]
|
||||||
|
|
||||||
|
cgroup_root = '/sys/fs/cgroup'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
return target.config.has('cgroups') and target.is_rooted
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
super(CgroupsModule, self).__init__(target)
|
||||||
|
mounted = self.target.list_file_systems()
|
||||||
|
if self.cgroup_root not in [e.mount_point for e in mounted]:
|
||||||
|
self.target.execute('mount -t tmpfs {} {}'.format('cgroup_root', self.cgroup_root),
|
||||||
|
as_root=True)
|
||||||
|
else:
|
||||||
|
self.logger.debug('cgroup_root already mounted at {}'.format(self.cgroup_root))
|
||||||
|
self.controllers = []
|
||||||
|
for cls in self.controller_cls:
|
||||||
|
controller = cls('devlib_{}'.format(cls.name))
|
||||||
|
if controller.probe(self.target):
|
||||||
|
if controller.mount_name in [e.device for e in mounted]:
|
||||||
|
self.logger.debug('controller {} is already mounted.'.format(controller.kind))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
controller.mount(self.target, self.cgroup_root)
|
||||||
|
except TargetError:
|
||||||
|
message = 'cgroups {} controller is not supported by the target'
|
||||||
|
raise TargetError(message.format(controller.kind))
|
||||||
|
|
||||||
|
def list_subsystems(self):
|
||||||
|
subsystems = []
|
||||||
|
for line in self.target.execute('cat /proc/cgroups').split('\n')[1:]:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
name, hierarchy, num_cgroups, enabled = line.split()
|
||||||
|
subsystems.append(CgroupSubsystemEntry(name,
|
||||||
|
int(hierarchy),
|
||||||
|
int(num_cgroups),
|
||||||
|
boolean(enabled)))
|
||||||
|
return subsystems
|
||||||
|
|
||||||
|
|
||||||
|
def get_cgroup_controller(self, kind):
|
||||||
|
for controller in self.controllers:
|
||||||
|
if controller.kind == kind:
|
||||||
|
return controller
|
||||||
|
raise ValueError(kind)
|
||||||
|
|
63
devlib/module/cooling.py
Normal file
63
devlib/module/cooling.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Copyright 2014-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
from devlib.module import Module
|
||||||
|
from devlib.utils.serial_port import open_serial_connection
|
||||||
|
|
||||||
|
|
||||||
|
class MbedFanActiveCoolingModule(Module):
|
||||||
|
|
||||||
|
name = 'mbed-fan'
|
||||||
|
timeout = 30
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __init__(self, target, port='/dev/ttyACM0', baud=115200, fan_pin=0):
|
||||||
|
super(MbedFanActiveCoolingModule, self).__init__(target)
|
||||||
|
self.port = port
|
||||||
|
self.baud = baud
|
||||||
|
self.fan_pin = fan_pin
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
with open_serial_connection(timeout=self.timeout,
|
||||||
|
port=self.port,
|
||||||
|
baudrate=self.baud) as target:
|
||||||
|
target.sendline('motor_{}_1'.format(self.fan_pin))
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
with open_serial_connection(timeout=self.timeout,
|
||||||
|
port=self.port,
|
||||||
|
baudrate=self.baud) as target:
|
||||||
|
target.sendline('motor_{}_0'.format(self.fan_pin))
|
||||||
|
|
||||||
|
|
||||||
|
class OdroidXU3ctiveCoolingModule(Module):
|
||||||
|
|
||||||
|
name = 'odroidxu3-fan'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
return target.file_exists('/sys/devices/odroid_fan.15/fan_mode')
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.target.write_value('/sys/devices/odroid_fan.15/fan_mode', 0, verify=False)
|
||||||
|
self.target.write_value('/sys/devices/odroid_fan.15/pwm_duty', 255, verify=False)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.target.write_value('/sys/devices/odroid_fan.15/fan_mode', 0, verify=False)
|
||||||
|
self.target.write_value('/sys/devices/odroid_fan.15/pwm_duty', 1, verify=False)
|
315
devlib/module/cpufreq.py
Normal file
315
devlib/module/cpufreq.py
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
# Copyright 2014-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
from devlib.module import Module
|
||||||
|
from devlib.exception import TargetError
|
||||||
|
from devlib.utils.misc import memoized
|
||||||
|
|
||||||
|
|
||||||
|
# a dict of governor name and a list of it tunables that can't be read
|
||||||
|
WRITE_ONLY_TUNABLES = {
|
||||||
|
'interactive': ['boostpulse']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CpufreqModule(Module):
|
||||||
|
|
||||||
|
name = 'cpufreq'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
path = '/sys/devices/system/cpu/cpufreq'
|
||||||
|
return target.file_exists(path)
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
super(CpufreqModule, self).__init__(target)
|
||||||
|
self._governor_tunables = {}
|
||||||
|
|
||||||
|
@memoized
|
||||||
|
def list_governors(self, cpu):
|
||||||
|
"""Returns a list of governors supported by the cpu."""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_available_governors'.format(cpu)
|
||||||
|
output = self.target.read_value(sysfile)
|
||||||
|
return output.strip().split()
|
||||||
|
|
||||||
|
def get_governor(self, cpu):
|
||||||
|
"""Returns the governor currently set for the specified CPU."""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_governor'.format(cpu)
|
||||||
|
return self.target.read_value(sysfile)
|
||||||
|
|
||||||
|
def set_governor(self, cpu, governor, **kwargs):
|
||||||
|
"""
|
||||||
|
Set the governor for the specified CPU.
|
||||||
|
See https://www.kernel.org/doc/Documentation/cpu-freq/governors.txt
|
||||||
|
|
||||||
|
:param cpu: The CPU for which the governor is to be set. This must be
|
||||||
|
the full name as it appears in sysfs, e.g. "cpu0".
|
||||||
|
:param governor: The name of the governor to be used. This must be
|
||||||
|
supported by the specific device.
|
||||||
|
|
||||||
|
Additional keyword arguments can be used to specify governor tunables for
|
||||||
|
governors that support them.
|
||||||
|
|
||||||
|
:note: On big.LITTLE all cores in a cluster must be using the same governor.
|
||||||
|
Setting the governor on any core in a cluster will also set it on all
|
||||||
|
other cores in that cluster.
|
||||||
|
|
||||||
|
:raises: TargetError if governor is not supported by the CPU, or if,
|
||||||
|
for some reason, the governor could not be set.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
supported = self.list_governors(cpu)
|
||||||
|
if governor not in supported:
|
||||||
|
raise TargetError('Governor {} not supported for cpu {}'.format(governor, cpu))
|
||||||
|
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_governor'.format(cpu)
|
||||||
|
self.target.write_value(sysfile, governor)
|
||||||
|
self.set_governor_tunables(cpu, governor, **kwargs)
|
||||||
|
|
||||||
|
def list_governor_tunables(self, cpu):
|
||||||
|
"""Returns a list of tunables available for the governor on the specified CPU."""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
governor = self.get_governor(cpu)
|
||||||
|
if governor not in self._governor_tunables:
|
||||||
|
try:
|
||||||
|
tunables_path = '/sys/devices/system/cpu/{}/cpufreq/{}'.format(cpu, governor)
|
||||||
|
self._governor_tunables[governor] = self.target.list_directory(tunables_path)
|
||||||
|
except TargetError: # probably an older kernel
|
||||||
|
try:
|
||||||
|
tunables_path = '/sys/devices/system/cpu/cpufreq/{}'.format(governor)
|
||||||
|
self._governor_tunables[governor] = self.target.list_directory(tunables_path)
|
||||||
|
except TargetError: # governor does not support tunables
|
||||||
|
self._governor_tunables[governor] = []
|
||||||
|
return self._governor_tunables[governor]
|
||||||
|
|
||||||
|
def get_governor_tunables(self, cpu):
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
governor = self.get_governor(cpu)
|
||||||
|
tunables = {}
|
||||||
|
for tunable in self.list_governor_tunables(cpu):
|
||||||
|
if tunable not in WRITE_ONLY_TUNABLES.get(governor, []):
|
||||||
|
try:
|
||||||
|
path = '/sys/devices/system/cpu/{}/cpufreq/{}/{}'.format(cpu, governor, tunable)
|
||||||
|
tunables[tunable] = self.target.read_value(path)
|
||||||
|
except TargetError: # May be an older kernel
|
||||||
|
path = '/sys/devices/system/cpu/cpufreq/{}/{}'.format(governor, tunable)
|
||||||
|
tunables[tunable] = self.target.read_value(path)
|
||||||
|
return tunables
|
||||||
|
|
||||||
|
def set_governor_tunables(self, cpu, governor=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Set tunables for the specified governor. Tunables should be specified as
|
||||||
|
keyword arguments. Which tunables and values are valid depends on the
|
||||||
|
governor.
|
||||||
|
|
||||||
|
:param cpu: The cpu for which the governor will be set. This must be the
|
||||||
|
full cpu name as it appears in sysfs, e.g. ``cpu0``.
|
||||||
|
:param governor: The name of the governor. Must be all lower case.
|
||||||
|
|
||||||
|
The rest should be keyword parameters mapping tunable name onto the value to
|
||||||
|
be set for it.
|
||||||
|
|
||||||
|
:raises: TargetError if governor specified is not a valid governor name, or if
|
||||||
|
a tunable specified is not valid for the governor, or if could not set
|
||||||
|
tunable.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
if governor is None:
|
||||||
|
governor = self.get_governor(cpu)
|
||||||
|
valid_tunables = self.list_governor_tunables(cpu)
|
||||||
|
for tunable, value in kwargs.iteritems():
|
||||||
|
if tunable in valid_tunables:
|
||||||
|
try:
|
||||||
|
path = '/sys/devices/system/cpu/{}/cpufreq/{}/{}'.format(cpu, governor, tunable)
|
||||||
|
self.target.write_value(path, value)
|
||||||
|
except TargetError: # May be an older kernel
|
||||||
|
path = '/sys/devices/system/cpu/cpufreq/{}/{}'.format(governor, tunable)
|
||||||
|
self.target.write_value(path, value)
|
||||||
|
else:
|
||||||
|
message = 'Unexpected tunable {} for governor {} on {}.\n'.format(tunable, governor, cpu)
|
||||||
|
message += 'Available tunables are: {}'.format(valid_tunables)
|
||||||
|
raise TargetError(message)
|
||||||
|
|
||||||
|
@memoized
|
||||||
|
def list_frequencies(self, cpu):
|
||||||
|
"""Returns a list of frequencies supported by the cpu or an empty list
|
||||||
|
if not could be found."""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
try:
|
||||||
|
cmd = 'cat /sys/devices/system/cpu/{}/cpufreq/scaling_available_frequencies'.format(cpu)
|
||||||
|
output = self.target.execute(cmd)
|
||||||
|
available_frequencies = map(int, output.strip().split()) # pylint: disable=E1103
|
||||||
|
except TargetError:
|
||||||
|
# On some devices scaling_frequencies is not generated.
|
||||||
|
# http://adrynalyne-teachtofish.blogspot.co.uk/2011/11/how-to-enable-scalingavailablefrequenci.html
|
||||||
|
# Fall back to parsing stats/time_in_state
|
||||||
|
cmd = 'cat /sys/devices/system/cpu/{}/cpufreq/stats/time_in_state'.format(cpu)
|
||||||
|
out_iter = iter(self.target.execute(cmd).strip().split())
|
||||||
|
available_frequencies = map(int, reversed([f for f, _ in zip(out_iter, out_iter)]))
|
||||||
|
return available_frequencies
|
||||||
|
|
||||||
|
def get_min_frequency(self, cpu):
|
||||||
|
"""
|
||||||
|
Returns the min frequency currently set for the specified CPU.
|
||||||
|
|
||||||
|
Warning, this method does not check if the cpu is online or not. It will
|
||||||
|
try to read the minimum frequency and the following exception will be
|
||||||
|
raised ::
|
||||||
|
|
||||||
|
:raises: TargetError if for some reason the frequency could not be read.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_min_freq'.format(cpu)
|
||||||
|
return self.target.read_int(sysfile)
|
||||||
|
|
||||||
|
def set_min_frequency(self, cpu, frequency, exact=True):
|
||||||
|
"""
|
||||||
|
Set's the minimum value for CPU frequency. Actual frequency will
|
||||||
|
depend on the Governor used and may vary during execution. The value should be
|
||||||
|
either an int or a string representing an integer. The Value must also be
|
||||||
|
supported by the device. The available frequencies can be obtained by calling
|
||||||
|
get_frequencies() or examining
|
||||||
|
|
||||||
|
/sys/devices/system/cpu/cpuX/cpufreq/scaling_frequencies
|
||||||
|
|
||||||
|
on the device.
|
||||||
|
|
||||||
|
:raises: TargetError if the frequency is not supported by the CPU, or if, for
|
||||||
|
some reason, frequency could not be set.
|
||||||
|
:raises: ValueError if ``frequency`` is not an integer.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
available_frequencies = self.list_frequencies(cpu)
|
||||||
|
try:
|
||||||
|
value = int(frequency)
|
||||||
|
if exact and available_frequencies and value not in available_frequencies:
|
||||||
|
raise TargetError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
|
||||||
|
value,
|
||||||
|
available_frequencies))
|
||||||
|
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_min_freq'.format(cpu)
|
||||||
|
self.target.write_value(sysfile, value)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('Frequency must be an integer; got: "{}"'.format(frequency))
|
||||||
|
|
||||||
|
def get_frequency(self, cpu):
|
||||||
|
"""
|
||||||
|
Returns the current frequency currently set for the specified CPU.
|
||||||
|
|
||||||
|
Warning, this method does not check if the cpu is online or not. It will
|
||||||
|
try to read the current frequency and the following exception will be
|
||||||
|
raised ::
|
||||||
|
|
||||||
|
:raises: TargetError if for some reason the frequency could not be read.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_cur_freq'.format(cpu)
|
||||||
|
return self.target.read_int(sysfile)
|
||||||
|
|
||||||
|
def set_frequency(self, cpu, frequency, exact=True):
|
||||||
|
"""
|
||||||
|
Set's the minimum value for CPU frequency. Actual frequency will
|
||||||
|
depend on the Governor used and may vary during execution. The value should be
|
||||||
|
either an int or a string representing an integer.
|
||||||
|
|
||||||
|
If ``exact`` flag is set (the default), the Value must also be supported by
|
||||||
|
the device. The available frequencies can be obtained by calling
|
||||||
|
get_frequencies() or examining
|
||||||
|
|
||||||
|
/sys/devices/system/cpu/cpuX/cpufreq/scaling_frequencies
|
||||||
|
|
||||||
|
on the device (if it exists).
|
||||||
|
|
||||||
|
:raises: TargetError if the frequency is not supported by the CPU, or if, for
|
||||||
|
some reason, frequency could not be set.
|
||||||
|
:raises: ValueError if ``frequency`` is not an integer.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
try:
|
||||||
|
value = int(frequency)
|
||||||
|
if exact:
|
||||||
|
available_frequencies = self.list_frequencies(cpu)
|
||||||
|
if available_frequencies and value not in available_frequencies:
|
||||||
|
raise TargetError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
|
||||||
|
value,
|
||||||
|
available_frequencies))
|
||||||
|
if self.get_governor(cpu) != 'userspace':
|
||||||
|
raise TargetError('Can\'t set {} frequency; governor must be "userspace"'.format(cpu))
|
||||||
|
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_setspeed'.format(cpu)
|
||||||
|
self.target.write_value(sysfile, value, verify=False)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('Frequency must be an integer; got: "{}"'.format(frequency))
|
||||||
|
|
||||||
|
def get_max_frequency(self, cpu):
|
||||||
|
"""
|
||||||
|
Returns the max frequency currently set for the specified CPU.
|
||||||
|
|
||||||
|
Warning, this method does not check if the cpu is online or not. It will
|
||||||
|
try to read the maximum frequency and the following exception will be
|
||||||
|
raised ::
|
||||||
|
|
||||||
|
:raises: TargetError if for some reason the frequency could not be read.
|
||||||
|
"""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_max_freq'.format(cpu)
|
||||||
|
return self.target.read_int(sysfile)
|
||||||
|
|
||||||
|
def set_max_frequency(self, cpu, frequency, exact=True):
|
||||||
|
"""
|
||||||
|
Set's the minimum value for CPU frequency. Actual frequency will
|
||||||
|
depend on the Governor used and may vary during execution. The value should be
|
||||||
|
either an int or a string representing an integer. The Value must also be
|
||||||
|
supported by the device. The available frequencies can be obtained by calling
|
||||||
|
get_frequencies() or examining
|
||||||
|
|
||||||
|
/sys/devices/system/cpu/cpuX/cpufreq/scaling_frequencies
|
||||||
|
|
||||||
|
on the device.
|
||||||
|
|
||||||
|
:raises: TargetError if the frequency is not supported by the CPU, or if, for
|
||||||
|
some reason, frequency could not be set.
|
||||||
|
:raises: ValueError if ``frequency`` is not an integer.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
available_frequencies = self.list_frequencies(cpu)
|
||||||
|
try:
|
||||||
|
value = int(frequency)
|
||||||
|
if exact and available_frequencies and value not in available_frequencies:
|
||||||
|
raise TargetError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
|
||||||
|
value,
|
||||||
|
available_frequencies))
|
||||||
|
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_max_freq'.format(cpu)
|
||||||
|
self.target.write_value(sysfile, value)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('Frequency must be an integer; got: "{}"'.format(frequency))
|
138
devlib/module/cpuidle.py
Normal file
138
devlib/module/cpuidle.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# Copyright 2014-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
|
from devlib.module import Module
|
||||||
|
from devlib.utils.misc import memoized
|
||||||
|
from devlib.utils.types import integer, boolean
|
||||||
|
|
||||||
|
|
||||||
|
class CpuidleState(object):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def usage(self):
|
||||||
|
return integer(self.get('usage'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time(self):
|
||||||
|
return integer(self.get('time'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_enabled(self):
|
||||||
|
return not boolean(self.get('disable'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ordinal(self):
|
||||||
|
i = len(self.id)
|
||||||
|
while self.id[i - 1].isdigit():
|
||||||
|
i -= 1
|
||||||
|
if not i:
|
||||||
|
raise ValueError('invalid idle state name: "{}"'.format(self.id))
|
||||||
|
return int(self.id[i:])
|
||||||
|
|
||||||
|
def __init__(self, target, index, path):
|
||||||
|
self.target = target
|
||||||
|
self.index = index
|
||||||
|
self.path = path
|
||||||
|
self.id = self.target.path.basename(self.path)
|
||||||
|
self.cpu = self.target.path.basename(self.target.path.dirname(path))
|
||||||
|
self.desc = self.get('desc')
|
||||||
|
self.name = self.get('name')
|
||||||
|
self.latency = self.get('latency')
|
||||||
|
self.power = self.get('power')
|
||||||
|
|
||||||
|
def enable(self):
|
||||||
|
self.set('disable', 0)
|
||||||
|
|
||||||
|
def disable(self):
|
||||||
|
self.set('disable', 1)
|
||||||
|
|
||||||
|
def get(self, prop):
|
||||||
|
property_path = self.target.path.join(self.path, prop)
|
||||||
|
return self.target.read_value(property_path)
|
||||||
|
|
||||||
|
def set(self, prop, value):
|
||||||
|
property_path = self.target.path.join(self.path, prop)
|
||||||
|
self.target.write_value(property_path, value)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, CpuidleState):
|
||||||
|
return (self.name == other.name) and (self.desc == other.desc)
|
||||||
|
elif isinstance(other, basestring):
|
||||||
|
return (self.name == other) or (self.desc == other)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'CpuidleState({}, {})'.format(self.name, self.desc)
|
||||||
|
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
|
||||||
|
class Cpuidle(Module):
|
||||||
|
|
||||||
|
name = 'cpuidle'
|
||||||
|
root_path = '/sys/devices/system/cpu/cpuidle'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
return target.file_exists(Cpuidle.root_path)
|
||||||
|
|
||||||
|
def get_driver(self):
|
||||||
|
return self.target.read_value(self.target.path.join(self.root_path, 'current_driver'))
|
||||||
|
|
||||||
|
def get_governor(self):
|
||||||
|
return self.target.read_value(self.target.path.join(self.root_path, 'current_governor_ro'))
|
||||||
|
|
||||||
|
@memoized
|
||||||
|
def get_states(self, cpu=0):
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
states_dir = self.target.path.join(self.target.path.dirname(self.root_path), cpu, 'cpuidle')
|
||||||
|
idle_states = []
|
||||||
|
for state in self.target.list_directory(states_dir):
|
||||||
|
if state.startswith('state'):
|
||||||
|
index = int(state[5:])
|
||||||
|
idle_states.append(CpuidleState(self.target, index, self.target.path.join(states_dir, state)))
|
||||||
|
return idle_states
|
||||||
|
|
||||||
|
def get_state(self, state, cpu=0):
|
||||||
|
if isinstance(state, int):
|
||||||
|
try:
|
||||||
|
self.get_states(cpu)[state].enable()
|
||||||
|
except IndexError:
|
||||||
|
raise ValueError('Cpuidle state {} does not exist'.format(state))
|
||||||
|
else: # assume string-like
|
||||||
|
for s in self.get_states(cpu):
|
||||||
|
if state in [s.id, s.name, s.desc]:
|
||||||
|
return s
|
||||||
|
raise ValueError('Cpuidle state {} does not exist'.format(state))
|
||||||
|
|
||||||
|
def enable(self, state, cpu=0):
|
||||||
|
self.get_state(state, cpu).enable()
|
||||||
|
|
||||||
|
def disable(self, state, cpu=0):
|
||||||
|
self.get_state(state, cpu).disable()
|
||||||
|
|
||||||
|
def enable_all(self, cpu=0):
|
||||||
|
for state in self.get_states(cpu):
|
||||||
|
state.enable()
|
||||||
|
|
||||||
|
def disable_all(self, cpu=0):
|
||||||
|
for state in self.get_states(cpu):
|
||||||
|
state.disable()
|
||||||
|
|
35
devlib/module/hotplug.py
Normal file
35
devlib/module/hotplug.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from devlib.module import Module
|
||||||
|
|
||||||
|
|
||||||
|
class HotplugModule(Module):
|
||||||
|
|
||||||
|
name = 'hotplug'
|
||||||
|
base_path = '/sys/devices/system/cpu'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def probe(cls, target): # pylint: disable=arguments-differ
|
||||||
|
path = cls._cpu_path(target, 0)
|
||||||
|
return target.file_exists(path) and target.is_rooted
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _cpu_path(cls, target, cpu):
|
||||||
|
if isinstance(cpu, int):
|
||||||
|
cpu = 'cpu{}'.format(cpu)
|
||||||
|
return target.path.join(cls.base_path, cpu, 'online')
|
||||||
|
|
||||||
|
def online_all(self):
|
||||||
|
self.online(*range(self.target.number_of_cpus))
|
||||||
|
|
||||||
|
def online(self, *args):
|
||||||
|
for cpu in args:
|
||||||
|
self.hotplug(cpu, online=True)
|
||||||
|
|
||||||
|
def offline(self, *args):
|
||||||
|
for cpu in args:
|
||||||
|
self.hotplug(cpu, online=False)
|
||||||
|
|
||||||
|
def hotplug(self, cpu, online):
|
||||||
|
path = self._cpu_path(self.target, cpu)
|
||||||
|
value = 1 if online else 0
|
||||||
|
self.target.write_value(path, value)
|
||||||
|
|
140
devlib/module/hwmon.py
Normal file
140
devlib/module/hwmon.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# Copyright 2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from devlib.module import Module
|
||||||
|
from devlib.utils.types import integer
|
||||||
|
|
||||||
|
|
||||||
|
HWMON_ROOT = '/sys/class/hwmon'
|
||||||
|
HWMON_FILE_REGEX = re.compile(r'(?P<kind>\w+?)(?P<number>\d+)_(?P<item>\w+)')
|
||||||
|
|
||||||
|
|
||||||
|
class HwmonSensor(object):
|
||||||
|
|
||||||
|
def __init__(self, device, path, kind, number):
|
||||||
|
self.device = device
|
||||||
|
self.path = path
|
||||||
|
self.kind = kind
|
||||||
|
self.number = number
|
||||||
|
self.target = self.device.target
|
||||||
|
self.name = '{}/{}{}'.format(self.device.name, self.kind, self.number)
|
||||||
|
self.label = self.name
|
||||||
|
self.items = set()
|
||||||
|
|
||||||
|
def add(self, item):
|
||||||
|
self.items.add(item)
|
||||||
|
if item == 'label':
|
||||||
|
self.label = self.get('label')
|
||||||
|
|
||||||
|
def get(self, item):
|
||||||
|
path = self.get_file(item)
|
||||||
|
value = self.target.read_value(path)
|
||||||
|
try:
|
||||||
|
return integer(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def set(self, item, value):
|
||||||
|
path = self.get_file(item)
|
||||||
|
self.target.write_value(path, value)
|
||||||
|
|
||||||
|
def get_file(self, item):
|
||||||
|
if item not in self.items:
|
||||||
|
raise ValueError('item "{}" does not exist for {}'.format(item, self.name))
|
||||||
|
filename = '{}{}_{}'.format(self.kind, self.number, item)
|
||||||
|
return self.target.path.join(self.path, filename)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.name != self.label:
|
||||||
|
text = 'HS({}, {})'.format(self.name, self.label)
|
||||||
|
else:
|
||||||
|
text = 'HS({})'.format(self.name)
|
||||||
|
return text
|
||||||
|
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
|
||||||
|
class HwmonDevice(object):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sensors(self):
|
||||||
|
all_sensors = []
|
||||||
|
for sensors_of_kind in self._sensors.itervalues():
|
||||||
|
all_sensors.extend(sensors_of_kind.values())
|
||||||
|
return all_sensors
|
||||||
|
|
||||||
|
def __init__(self, target, path):
|
||||||
|
self.target = target
|
||||||
|
self.path = path
|
||||||
|
self.name = self.target.read_value(self.target.path.join(self.path, 'name'))
|
||||||
|
self._sensors = defaultdict(dict)
|
||||||
|
path = self.path
|
||||||
|
if not path.endswith(self.target.path.sep):
|
||||||
|
path += self.target.path.sep
|
||||||
|
for entry in self.target.list_directory(path):
|
||||||
|
match = HWMON_FILE_REGEX.search(entry)
|
||||||
|
if match:
|
||||||
|
kind = match.group('kind')
|
||||||
|
number = int(match.group('number'))
|
||||||
|
item = match.group('item')
|
||||||
|
if number not in self._sensors[kind]:
|
||||||
|
sensor = HwmonSensor(self, self.path, kind, number)
|
||||||
|
self._sensors[kind][number] = sensor
|
||||||
|
self._sensors[kind][number].add(item)
|
||||||
|
|
||||||
|
def get(self, kind, number=None):
|
||||||
|
if number is None:
|
||||||
|
return [s for _, s in sorted(self._sensors[kind].iteritems(),
|
||||||
|
key=lambda x: x[0])]
|
||||||
|
else:
|
||||||
|
return self._sensors[kind].get(number)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'HD({})'.format(self.name)
|
||||||
|
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
|
||||||
|
class HwmonModule(Module):
|
||||||
|
|
||||||
|
name = 'hwmon'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
return target.file_exists(HWMON_ROOT)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sensors(self):
|
||||||
|
all_sensors = []
|
||||||
|
for device in self.devices:
|
||||||
|
all_sensors.extend(device.sensors)
|
||||||
|
return all_sensors
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
super(HwmonModule, self).__init__(target)
|
||||||
|
self.root = HWMON_ROOT
|
||||||
|
self.devices = []
|
||||||
|
self.scan()
|
||||||
|
|
||||||
|
def scan(self):
|
||||||
|
for entry in self.target.list_directory(self.root):
|
||||||
|
if entry.startswith('hwmon'):
|
||||||
|
entry_path = self.target.path.join(self.root, entry)
|
||||||
|
if self.target.file_exists(self.target.path.join(entry_path, 'name')):
|
||||||
|
device = HwmonDevice(self.target, entry_path)
|
||||||
|
self.devices.append(device)
|
||||||
|
|
386
devlib/module/vexpress.py
Normal file
386
devlib/module/vexpress.py
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
#
|
||||||
|
# Copyright 2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import tarfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from devlib.module import HardRestModule, BootModule, FlashModule
|
||||||
|
from devlib.exception import TargetError, HostError
|
||||||
|
from devlib.utils.serial_port import open_serial_connection, pulse_dtr, write_characters
|
||||||
|
from devlib.utils.uefi import UefiMenu, UefiConfig
|
||||||
|
from devlib.utils.uboot import UbootMenu
|
||||||
|
|
||||||
|
|
||||||
|
AUTOSTART_MESSAGE = 'Press Enter to stop auto boot...'
|
||||||
|
POWERUP_MESSAGE = 'Powering up system...'
|
||||||
|
DEFAULT_MCC_PROMPT = 'Cmd>'
|
||||||
|
|
||||||
|
|
||||||
|
class VexpressDtrHardReset(HardRestModule):
|
||||||
|
|
||||||
|
name = 'vexpress-dtr'
|
||||||
|
stage = 'early'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __init__(self, target, port='/dev/ttyS0', baudrate=115200,
|
||||||
|
mcc_prompt=DEFAULT_MCC_PROMPT, timeout=300):
|
||||||
|
super(VexpressDtrHardReset, self).__init__(target)
|
||||||
|
self.port = port
|
||||||
|
self.baudrate = baudrate
|
||||||
|
self.mcc_prompt = mcc_prompt
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
try:
|
||||||
|
if self.target.is_connected:
|
||||||
|
self.target.execute('sync')
|
||||||
|
except TargetError:
|
||||||
|
pass
|
||||||
|
with open_serial_connection(port=self.port,
|
||||||
|
baudrate=self.baudrate,
|
||||||
|
timeout=self.timeout,
|
||||||
|
init_dtr=0,
|
||||||
|
get_conn=True) as (_, conn):
|
||||||
|
pulse_dtr(conn, state=True, duration=0.1) # TRM specifies a pulse of >=100ms
|
||||||
|
|
||||||
|
|
||||||
|
class VexpressReboottxtHardReset(HardRestModule):
|
||||||
|
|
||||||
|
name = 'vexpress-reboottxt'
|
||||||
|
stage = 'early'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __init__(self, target,
|
||||||
|
port='/dev/ttyS0', baudrate=115200,
|
||||||
|
path='/media/VEMSD',
|
||||||
|
mcc_prompt=DEFAULT_MCC_PROMPT, timeout=30, short_delay=1):
|
||||||
|
super(VexpressReboottxtHardReset, self).__init__(target)
|
||||||
|
self.port = port
|
||||||
|
self.baudrate = baudrate
|
||||||
|
self.path = path
|
||||||
|
self.mcc_prompt = mcc_prompt
|
||||||
|
self.timeout = timeout
|
||||||
|
self.short_delay = short_delay
|
||||||
|
self.filepath = os.path.join(path, 'reboot.txt')
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
try:
|
||||||
|
if self.target.is_connected:
|
||||||
|
self.target.execute('sync')
|
||||||
|
except TargetError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
self.logger.debug('{} does not exisit; attempting to mount...'.format(self.path))
|
||||||
|
with open_serial_connection(port=self.port,
|
||||||
|
baudrate=self.baudrate,
|
||||||
|
timeout=self.timeout,
|
||||||
|
init_dtr=0) as tty:
|
||||||
|
wait_for_vemsd(self.path, tty, self.mcc_prompt, self.short_delay)
|
||||||
|
with open(self.filepath, 'w'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VexpressBootModule(BootModule):
|
||||||
|
|
||||||
|
stage = 'early'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __init__(self, target, uefi_entry=None,
|
||||||
|
port='/dev/ttyS0', baudrate=115200,
|
||||||
|
mcc_prompt=DEFAULT_MCC_PROMPT,
|
||||||
|
timeout=120, short_delay=1):
|
||||||
|
super(VexpressBootModule, self).__init__(target)
|
||||||
|
self.port = port
|
||||||
|
self.baudrate = baudrate
|
||||||
|
self.uefi_entry = uefi_entry
|
||||||
|
self.mcc_prompt = mcc_prompt
|
||||||
|
self.timeout = timeout
|
||||||
|
self.short_delay = short_delay
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
with open_serial_connection(port=self.port,
|
||||||
|
baudrate=self.baudrate,
|
||||||
|
timeout=self.timeout,
|
||||||
|
init_dtr=0) as tty:
|
||||||
|
self.get_through_early_boot(tty)
|
||||||
|
self.perform_boot_sequence(tty)
|
||||||
|
self.wait_for_android_prompt(tty)
|
||||||
|
|
||||||
|
def perform_boot_sequence(self, tty):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_through_early_boot(self, tty):
|
||||||
|
self.logger.debug('Establishing initial state...')
|
||||||
|
tty.sendline('')
|
||||||
|
i = tty.expect([AUTOSTART_MESSAGE, POWERUP_MESSAGE, self.mcc_prompt])
|
||||||
|
if i == 2:
|
||||||
|
self.logger.debug('Saw MCC prompt.')
|
||||||
|
time.sleep(self.short_delay)
|
||||||
|
tty.sendline('reboot')
|
||||||
|
elif i == 1:
|
||||||
|
self.logger.debug('Saw powering up message (assuming soft reboot).')
|
||||||
|
else:
|
||||||
|
self.logger.debug('Saw auto boot message.')
|
||||||
|
tty.sendline('')
|
||||||
|
time.sleep(self.short_delay)
|
||||||
|
tty.sendline('reboot')
|
||||||
|
|
||||||
|
def get_uefi_menu(self, tty):
|
||||||
|
menu = UefiMenu(tty)
|
||||||
|
self.logger.debug('Waiting for UEFI menu...')
|
||||||
|
menu.wait(timeout=self.timeout)
|
||||||
|
return menu
|
||||||
|
|
||||||
|
def wait_for_android_prompt(self, tty):
|
||||||
|
self.logger.debug('Waiting for the Android prompt.')
|
||||||
|
tty.expect(self.target.shell_prompt, timeout=self.timeout)
|
||||||
|
# This delay is needed to allow the platform some time to finish
|
||||||
|
# initilizing; querying the ip address too early from connect() may
|
||||||
|
# result in a bogus address being assigned to eth0.
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
class VexpressUefiBoot(VexpressBootModule):
|
||||||
|
|
||||||
|
name = 'vexpress-uefi'
|
||||||
|
|
||||||
|
def __init__(self, target, uefi_entry,
|
||||||
|
image, fdt, bootargs, initrd,
|
||||||
|
*args, **kwargs):
|
||||||
|
super(VexpressUefiBoot, self).__init__(target, uefi_entry=uefi_entry,
|
||||||
|
*args, **kwargs)
|
||||||
|
self.uefi_config = self._create_config(image, fdt, bootargs, initrd)
|
||||||
|
|
||||||
|
def perform_boot_sequence(self, tty):
|
||||||
|
menu = self.get_uefi_menu(tty)
|
||||||
|
try:
|
||||||
|
menu.select(self.uefi_entry)
|
||||||
|
except LookupError:
|
||||||
|
self.logger.debug('{} UEFI entry not found.'.format(self.uefi_entry))
|
||||||
|
self.logger.debug('Attempting to create one using default flasher configuration.')
|
||||||
|
menu.create_entry(self.uefi_entry, self.uefi_config)
|
||||||
|
menu.select(self.uefi_entry)
|
||||||
|
|
||||||
|
def _create_config(self, image, fdt, bootargs, initrd): # pylint: disable=R0201
|
||||||
|
config_dict = {
|
||||||
|
'image_name': image,
|
||||||
|
'image_args': bootargs,
|
||||||
|
'initrd': initrd,
|
||||||
|
}
|
||||||
|
|
||||||
|
if fdt:
|
||||||
|
config_dict['fdt_support'] = True
|
||||||
|
config_dict['fdt_path'] = fdt
|
||||||
|
else:
|
||||||
|
config_dict['fdt_support'] = False
|
||||||
|
|
||||||
|
return UefiConfig(config_dict)
|
||||||
|
|
||||||
|
|
||||||
|
class VexpressUefiShellBoot(VexpressBootModule):
|
||||||
|
|
||||||
|
name = 'vexpress-uefi-shell'
|
||||||
|
|
||||||
|
def __init__(self, target, uefi_entry='^Shell$',
|
||||||
|
efi_shell_prompt='Shell>',
|
||||||
|
image='kernel', bootargs=None,
|
||||||
|
*args, **kwargs):
|
||||||
|
super(VexpressUefiShellBoot, self).__init__(target, uefi_entry=uefi_entry,
|
||||||
|
*args, **kwargs)
|
||||||
|
self.efi_shell_prompt = efi_shell_prompt
|
||||||
|
self.image = image
|
||||||
|
self.bootargs = bootargs
|
||||||
|
|
||||||
|
def perform_boot_sequence(self, tty):
|
||||||
|
menu = self.get_uefi_menu(tty)
|
||||||
|
try:
|
||||||
|
menu.select(self.uefi_entry)
|
||||||
|
except LookupError:
|
||||||
|
raise TargetError('Did not see "{}" UEFI entry.'.format(self.uefi_entry))
|
||||||
|
tty.expect(self.efi_shell_prompt, timeout=self.timeout)
|
||||||
|
if self.bootargs:
|
||||||
|
tty.sendline('') # stop default boot
|
||||||
|
time.sleep(self.short_delay)
|
||||||
|
efi_shell_command = '{} {}'.format(self.image, self.bootargs)
|
||||||
|
self.logger.debug(efi_shell_command)
|
||||||
|
write_characters(tty, efi_shell_command)
|
||||||
|
tty.sendline('\r\n')
|
||||||
|
|
||||||
|
|
||||||
|
class VexpressUBoot(VexpressBootModule):
|
||||||
|
|
||||||
|
name = 'vexpress-u-boot'
|
||||||
|
|
||||||
|
def __init__(self, target, env=None,
|
||||||
|
*args, **kwargs):
|
||||||
|
super(VexpressUBoot, self).__init__(target, *args, **kwargs)
|
||||||
|
self.env = env
|
||||||
|
|
||||||
|
def perform_boot_sequence(self, tty):
|
||||||
|
if self.env is None:
|
||||||
|
return # Will boot automatically
|
||||||
|
|
||||||
|
menu = UbootMenu(tty)
|
||||||
|
self.logger.debug('Waiting for U-Boot prompt...')
|
||||||
|
menu.open(timeout=120)
|
||||||
|
for var, value in self.env.iteritems():
|
||||||
|
menu.setenv(var, value)
|
||||||
|
menu.boot()
|
||||||
|
|
||||||
|
|
||||||
|
class VexpressBootmon(VexpressBootModule):
|
||||||
|
|
||||||
|
name = 'vexpress-bootmon'
|
||||||
|
|
||||||
|
def __init__(self, target,
|
||||||
|
image, fdt, initrd, bootargs,
|
||||||
|
uses_bootscript=False,
|
||||||
|
bootmon_prompt='>',
|
||||||
|
*args, **kwargs):
|
||||||
|
super(VexpressBootmon, self).__init__(target, *args, **kwargs)
|
||||||
|
self.image = image
|
||||||
|
self.fdt = fdt
|
||||||
|
self.initrd = initrd
|
||||||
|
self.bootargs = bootargs
|
||||||
|
self.uses_bootscript = uses_bootscript
|
||||||
|
self.bootmon_prompt = bootmon_prompt
|
||||||
|
|
||||||
|
def perform_boot_sequence(self, tty):
|
||||||
|
if self.uses_bootscript:
|
||||||
|
return # Will boot automatically
|
||||||
|
|
||||||
|
time.sleep(self.short_delay)
|
||||||
|
tty.expect(self.bootmon_prompt, timeout=self.timeout)
|
||||||
|
with open_serial_connection(port=self.port,
|
||||||
|
baudrate=self.baudrate,
|
||||||
|
timeout=self.timeout,
|
||||||
|
init_dtr=0) as tty:
|
||||||
|
write_characters(tty, 'fl linux fdt {}'.format(self.fdt))
|
||||||
|
write_characters(tty, 'fl linux initrd {}'.format(self.initrd))
|
||||||
|
write_characters(tty, 'fl linux boot {} {}'.format(self.image,
|
||||||
|
self.bootargs))
|
||||||
|
|
||||||
|
|
||||||
|
class VersatileExpressFlashModule(FlashModule):
|
||||||
|
|
||||||
|
name = 'vexpress-vemsd'
|
||||||
|
description = """
|
||||||
|
Enables flashing of kernels and firmware to ARM Versatile Express devices.
|
||||||
|
|
||||||
|
This modules enables flashing of image bundles or individual images to ARM
|
||||||
|
Versatile Express-based devices (e.g. JUNO) via host-mounted MicroSD on the
|
||||||
|
board.
|
||||||
|
|
||||||
|
The bundle, if specified, must reflect the directory structure of the MicroSD
|
||||||
|
and will be extracted directly into the location it is mounted on the host. The
|
||||||
|
images, if specified, must be a dict mapping the absolute path of the image on
|
||||||
|
the host to the destination path within the board's MicroSD; the destination path
|
||||||
|
may be either absolute, or relative to the MicroSD mount location.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
stage = 'early'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def probe(target):
|
||||||
|
if not target.has('hard_reset'):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __init__(self, target, vemsd_mount, mcc_prompt=DEFAULT_MCC_PROMPT, timeout=30, short_delay=1):
|
||||||
|
super(VersatileExpressFlashModule, self).__init__(target)
|
||||||
|
self.vemsd_mount = vemsd_mount
|
||||||
|
self.mcc_prompt = mcc_prompt
|
||||||
|
self.timeout = timeout
|
||||||
|
self.short_delay = short_delay
|
||||||
|
|
||||||
|
def __call__(self, image_bundle=None, images=None, bootargs=None):
|
||||||
|
self.target.hard_reset()
|
||||||
|
with open_serial_connection(port=self.target.platform.serial_port,
|
||||||
|
baudrate=self.target.platform.baudrate,
|
||||||
|
timeout=self.timeout,
|
||||||
|
init_dtr=0) as tty:
|
||||||
|
i = tty.expect([self.mcc_prompt, AUTOSTART_MESSAGE])
|
||||||
|
if i:
|
||||||
|
tty.sendline('')
|
||||||
|
wait_for_vemsd(self.vemsd_mount, tty, self.mcc_prompt, self.short_delay)
|
||||||
|
try:
|
||||||
|
if image_bundle:
|
||||||
|
self._deploy_image_bundle(image_bundle)
|
||||||
|
if images:
|
||||||
|
self._overlay_images(images)
|
||||||
|
os.system('sync')
|
||||||
|
except (IOError, OSError), e:
|
||||||
|
msg = 'Could not deploy images to {}; got: {}'
|
||||||
|
raise TargetError(msg.format(self.vemsd_mount, e))
|
||||||
|
self.target.boot()
|
||||||
|
self.target.connect(timeout=30)
|
||||||
|
|
||||||
|
def _deploy_image_bundle(self, bundle):
|
||||||
|
self.logger.debug('Validating {}'.format(bundle))
|
||||||
|
validate_image_bundle(bundle)
|
||||||
|
self.logger.debug('Extracting {} into {}...'.format(bundle, self.vemsd_mount))
|
||||||
|
with tarfile.open(bundle) as tar:
|
||||||
|
tar.extractall(self.vemsd_mount)
|
||||||
|
|
||||||
|
def _overlay_images(self, images):
|
||||||
|
for dest, src in images.iteritems():
|
||||||
|
dest = os.path.join(self.vemsd_mount, dest)
|
||||||
|
self.logger.debug('Copying {} to {}'.format(src, dest))
|
||||||
|
shutil.copy(src, dest)
|
||||||
|
|
||||||
|
|
||||||
|
# utility functions
|
||||||
|
|
||||||
|
def validate_image_bundle(bundle):
|
||||||
|
if not tarfile.is_tarfile(bundle):
|
||||||
|
raise HostError('Image bundle {} does not appear to be a valid TAR file.'.format(bundle))
|
||||||
|
with tarfile.open(bundle) as tar:
|
||||||
|
try:
|
||||||
|
tar.getmember('config.txt')
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
tar.getmember('./config.txt')
|
||||||
|
except KeyError:
|
||||||
|
msg = 'Tarball {} does not appear to be a valid image bundle (did not see config.txt).'
|
||||||
|
raise HostError(msg.format(bundle))
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_vemsd(vemsd_mount, tty, mcc_prompt=DEFAULT_MCC_PROMPT, short_delay=1, retries=3):
|
||||||
|
attempts = 1 + retries
|
||||||
|
path = os.path.join(vemsd_mount, 'config.txt')
|
||||||
|
if os.path.exists(path):
|
||||||
|
return
|
||||||
|
for _ in xrange(attempts):
|
||||||
|
tty.sendline('') # clear any garbage
|
||||||
|
tty.expect(mcc_prompt, timeout=short_delay)
|
||||||
|
tty.sendline('usb_on')
|
||||||
|
time.sleep(short_delay * 3)
|
||||||
|
if os.path.exists(path):
|
||||||
|
return
|
||||||
|
raise TargetError('Could not mount {}'.format(vemsd_mount))
|
||||||
|
|
81
devlib/platform/__init__.py
Normal file
81
devlib/platform/__init__.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Platform(object):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def number_of_clusters(self):
|
||||||
|
return len(set(self.core_clusters))
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
name=None,
|
||||||
|
core_names=None,
|
||||||
|
core_clusters=None,
|
||||||
|
big_core=None,
|
||||||
|
model=None,
|
||||||
|
modules=None,
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.core_names = core_names or []
|
||||||
|
self.core_clusters = core_clusters or []
|
||||||
|
self.big_core = big_core
|
||||||
|
self.little_core = None
|
||||||
|
self.model = model
|
||||||
|
self.modules = modules or []
|
||||||
|
self.logger = logging.getLogger(self.name)
|
||||||
|
if not self.core_clusters and self.core_names:
|
||||||
|
self._set_core_clusters_from_core_names()
|
||||||
|
self._validate()
|
||||||
|
|
||||||
|
def init_target_connection(self, target):
|
||||||
|
# May be ovewritten by subclasses to provide target-specific
|
||||||
|
# connection initialisation.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_from_target(self, target):
|
||||||
|
if not self.core_names:
|
||||||
|
self.core_names = target.cpuinfo.cpu_names
|
||||||
|
self._set_core_clusters_from_core_names()
|
||||||
|
if not self.big_core and self.number_of_clusters == 2:
|
||||||
|
big_idx = self.core_clusters.index(max(self.core_clusters))
|
||||||
|
self.big_core = self.core_names[big_idx]
|
||||||
|
if not self.core_clusters and self.core_names:
|
||||||
|
self._set_core_clusters_from_core_names()
|
||||||
|
if not self.model:
|
||||||
|
self._set_model_from_target(target)
|
||||||
|
if not self.name:
|
||||||
|
self.name = self.model
|
||||||
|
self._validate()
|
||||||
|
|
||||||
|
def _set_core_clusters_from_core_names(self):
|
||||||
|
self.core_clusters = []
|
||||||
|
clusters = []
|
||||||
|
for cn in self.core_names:
|
||||||
|
if cn not in clusters:
|
||||||
|
clusters.append(cn)
|
||||||
|
self.core_clusters.append(clusters.index(cn))
|
||||||
|
|
||||||
|
def _set_model_from_target(self, target):
|
||||||
|
if target.os == 'android':
|
||||||
|
self.model = target.getprop('ro.product.model')
|
||||||
|
elif target.is_rooted:
|
||||||
|
try:
|
||||||
|
self.model = target.execute('dmidecode -s system-version',
|
||||||
|
as_root=True).strip()
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
pass # this is best-effort
|
||||||
|
|
||||||
|
def _validate(self):
|
||||||
|
if len(self.core_names) != len(self.core_clusters):
|
||||||
|
raise ValueError('core_names and core_clusters are of different lengths.')
|
||||||
|
if self.big_core and self.number_of_clusters != 2:
|
||||||
|
raise ValueError('attempting to set big_core on non-big.LITTLE device. '
|
||||||
|
'(number of clusters is not 2)')
|
||||||
|
if self.big_core and self.big_core not in self.core_names:
|
||||||
|
message = 'Invalid big_core value "{}"; must be in [{}]'
|
||||||
|
raise ValueError(message.format(self.big_core,
|
||||||
|
', '.join(set(self.core_names))))
|
||||||
|
if self.big_core:
|
||||||
|
little_idx = self.core_clusters.index(min(self.core_clusters))
|
||||||
|
self.little_core = self.core_names[little_idx]
|
||||||
|
|
280
devlib/platform/arm.py
Normal file
280
devlib/platform/arm.py
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
# Copyright 2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
from __future__ import division
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import csv
|
||||||
|
import time
|
||||||
|
import pexpect
|
||||||
|
|
||||||
|
from devlib.platform import Platform
|
||||||
|
from devlib.instrument import Instrument, InstrumentChannel, MeasurementsCsv, CONTINUOUS
|
||||||
|
from devlib.exception import TargetError, HostError
|
||||||
|
from devlib.host import PACKAGE_BIN_DIRECTORY
|
||||||
|
from devlib.utils.serial_port import open_serial_connection
|
||||||
|
|
||||||
|
|
||||||
|
class VersatileExpressPlatform(Platform):
|
||||||
|
|
||||||
|
def __init__(self, name, # pylint: disable=too-many-locals
|
||||||
|
|
||||||
|
core_names=None,
|
||||||
|
core_clusters=None,
|
||||||
|
big_core=None,
|
||||||
|
modules=None,
|
||||||
|
|
||||||
|
# serial settings
|
||||||
|
serial_port='/dev/ttyS0',
|
||||||
|
baudrate=115200,
|
||||||
|
|
||||||
|
# VExpress MicroSD mount point
|
||||||
|
vemsd_mount=None,
|
||||||
|
|
||||||
|
# supported: dtr, reboottxt
|
||||||
|
hard_reset_method=None,
|
||||||
|
# supported: uefi, uefi-shell, u-boot, bootmon
|
||||||
|
bootloader=None,
|
||||||
|
# supported: vemsd
|
||||||
|
flash_method='vemsd',
|
||||||
|
|
||||||
|
image=None,
|
||||||
|
fdt=None,
|
||||||
|
initrd=None,
|
||||||
|
bootargs=None,
|
||||||
|
|
||||||
|
uefi_entry=None, # only used if bootloader is "uefi"
|
||||||
|
ready_timeout=60,
|
||||||
|
):
|
||||||
|
super(VersatileExpressPlatform, self).__init__(name,
|
||||||
|
core_names,
|
||||||
|
core_clusters,
|
||||||
|
big_core,
|
||||||
|
modules)
|
||||||
|
self.serial_port = serial_port
|
||||||
|
self.baudrate = baudrate
|
||||||
|
self.vemsd_mount = vemsd_mount
|
||||||
|
self.image = image
|
||||||
|
self.fdt = fdt
|
||||||
|
self.initrd = initrd
|
||||||
|
self.bootargs = bootargs
|
||||||
|
self.uefi_entry = uefi_entry
|
||||||
|
self.ready_timeout = ready_timeout
|
||||||
|
self.bootloader = None
|
||||||
|
self.hard_reset_method = None
|
||||||
|
self._set_bootloader(bootloader)
|
||||||
|
self._set_hard_reset_method(hard_reset_method)
|
||||||
|
self._set_flash_method(flash_method)
|
||||||
|
|
||||||
|
def init_target_connection(self, target):
|
||||||
|
if target.os == 'android':
|
||||||
|
self._init_android_target(target)
|
||||||
|
else:
|
||||||
|
self._init_linux_target(target)
|
||||||
|
|
||||||
|
def _init_android_target(self, target):
|
||||||
|
if target.connection_settings.get('device') is None:
|
||||||
|
addr = self._get_target_ip_address(target)
|
||||||
|
target.connection_settings['device'] = addr + ':5555'
|
||||||
|
|
||||||
|
def _init_linux_target(self, target):
|
||||||
|
if target.connection_settings.get('host') is None:
|
||||||
|
addr = self._get_target_ip_address(target)
|
||||||
|
target.connection_settings['host'] = addr
|
||||||
|
|
||||||
|
def _get_target_ip_address(self, target):
|
||||||
|
with open_serial_connection(port=self.serial_port,
|
||||||
|
baudrate=self.baudrate,
|
||||||
|
timeout=30,
|
||||||
|
init_dtr=0) as tty:
|
||||||
|
tty.sendline('')
|
||||||
|
self.logger.debug('Waiting for the Android shell prompt.')
|
||||||
|
tty.expect(target.shell_prompt)
|
||||||
|
|
||||||
|
self.logger.debug('Waiting for IP address...')
|
||||||
|
wait_start_time = time.time()
|
||||||
|
while True:
|
||||||
|
tty.sendline('ip addr list eth0')
|
||||||
|
time.sleep(1)
|
||||||
|
try:
|
||||||
|
tty.expect(r'inet ([1-9]\d*.\d+.\d+.\d+)', timeout=10)
|
||||||
|
return tty.match.group(1)
|
||||||
|
except pexpect.TIMEOUT:
|
||||||
|
pass # We have our own timeout -- see below.
|
||||||
|
if (time.time() - wait_start_time) > self.ready_timeout:
|
||||||
|
raise TargetError('Could not acquire IP address.')
|
||||||
|
|
||||||
|
def _set_hard_reset_method(self, hard_reset_method):
|
||||||
|
if hard_reset_method == 'dtr':
|
||||||
|
self.modules.append({'vexpress-dtr': {'port': self.serial_port,
|
||||||
|
'baudrate': self.baudrate,
|
||||||
|
}})
|
||||||
|
elif hard_reset_method == 'reboottxt':
|
||||||
|
self.modules.append({'vexpress-reboottxt': {'port': self.serial_port,
|
||||||
|
'baudrate': self.baudrate,
|
||||||
|
'path': self.vemsd_mount,
|
||||||
|
}})
|
||||||
|
else:
|
||||||
|
ValueError('Invalid hard_reset_method: {}'.format(hard_reset_method))
|
||||||
|
|
||||||
|
def _set_bootloader(self, bootloader):
|
||||||
|
self.bootloader = bootloader
|
||||||
|
if self.bootloader == 'uefi':
|
||||||
|
self.modules.append({'vexpress-uefi': {'port': self.serial_port,
|
||||||
|
'baudrate': self.baudrate,
|
||||||
|
'image': self.image,
|
||||||
|
'fdt': self.fdt,
|
||||||
|
'initrd': self.initrd,
|
||||||
|
'bootargs': self.bootargs,
|
||||||
|
}})
|
||||||
|
elif self.bootloader == 'uefi-shell':
|
||||||
|
self.modules.append({'vexpress-uefi-shell': {'port': self.serial_port,
|
||||||
|
'baudrate': self.baudrate,
|
||||||
|
'image': self.image,
|
||||||
|
'bootargs': self.bootargs,
|
||||||
|
}})
|
||||||
|
elif self.bootloader == 'u-boot':
|
||||||
|
self.modules.append({'vexpress-u-boot': {'port': self.serial_port,
|
||||||
|
'baudrate': self.baudrate,
|
||||||
|
'env': {'bootargs': self.bootargs},
|
||||||
|
}})
|
||||||
|
elif self.bootloader == 'bootmon':
|
||||||
|
self.modules.append({'vexpress-bootmon': {'port': self.serial_port,
|
||||||
|
'baudrate': self.baudrate,
|
||||||
|
'image': self.image,
|
||||||
|
'fdt': self.fdt,
|
||||||
|
'initrd': self.initrd,
|
||||||
|
'bootargs': self.bootargs,
|
||||||
|
}})
|
||||||
|
else:
|
||||||
|
ValueError('Invalid hard_reset_method: {}'.format(bootloader))
|
||||||
|
|
||||||
|
def _set_flash_method(self, flash_method):
|
||||||
|
if flash_method == 'vemsd':
|
||||||
|
self.modules.append({'vexpress-vemsd': {'vemsd_mount': self.vemsd_mount}})
|
||||||
|
else:
|
||||||
|
ValueError('Invalid flash_method: {}'.format(flash_method))
|
||||||
|
|
||||||
|
|
||||||
|
class Juno(VersatileExpressPlatform):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
vemsd_mount='/media/JUNO',
|
||||||
|
baudrate=115200,
|
||||||
|
bootloader='u-boot',
|
||||||
|
hard_reset_method='dtr',
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super(Juno, self).__init__('juno',
|
||||||
|
vemsd_mount=vemsd_mount,
|
||||||
|
baudrate=baudrate,
|
||||||
|
bootloader=bootloader,
|
||||||
|
hard_reset_method=hard_reset_method,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TC2(VersatileExpressPlatform):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
vemsd_mount='/media/VEMSD',
|
||||||
|
baudrate=38400,
|
||||||
|
bootloader='bootmon',
|
||||||
|
hard_reset_method='reboottxt',
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super(TC2, self).__init__('tc2',
|
||||||
|
vemsd_mount=vemsd_mount,
|
||||||
|
baudrate=baudrate,
|
||||||
|
bootloader=bootloader,
|
||||||
|
hard_reset_method=hard_reset_method,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class JunoEnergyInstrument(Instrument):
|
||||||
|
|
||||||
|
binname = 'readenergy'
|
||||||
|
mode = CONTINUOUS
|
||||||
|
|
||||||
|
_channels = [
|
||||||
|
InstrumentChannel('sys_curr', 'sys', 'current'),
|
||||||
|
InstrumentChannel('a57_curr', 'a57', 'current'),
|
||||||
|
InstrumentChannel('a53_curr', 'a53', 'current'),
|
||||||
|
InstrumentChannel('gpu_curr', 'gpu', 'current'),
|
||||||
|
InstrumentChannel('sys_volt', 'sys', 'voltage'),
|
||||||
|
InstrumentChannel('a57_volt', 'a57', 'voltage'),
|
||||||
|
InstrumentChannel('a53_volt', 'a53', 'voltage'),
|
||||||
|
InstrumentChannel('gpu_volt', 'gpu', 'voltage'),
|
||||||
|
InstrumentChannel('sys_pow', 'sys', 'power'),
|
||||||
|
InstrumentChannel('a57_pow', 'a57', 'power'),
|
||||||
|
InstrumentChannel('a53_pow', 'a53', 'power'),
|
||||||
|
InstrumentChannel('gpu_pow', 'gpu', 'power'),
|
||||||
|
InstrumentChannel('sys_cenr', 'sys', 'energy'),
|
||||||
|
InstrumentChannel('a57_cenr', 'a57', 'energy'),
|
||||||
|
InstrumentChannel('a53_cenr', 'a53', 'energy'),
|
||||||
|
InstrumentChannel('gpu_cenr', 'gpu', 'energy'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
super(JunoEnergyInstrument, self).__init__(target)
|
||||||
|
self.on_target_file = None
|
||||||
|
self.command = None
|
||||||
|
self.binary = self.target.bin(self.binname)
|
||||||
|
for chan in self._channels:
|
||||||
|
self.channels[chan.name] = chan
|
||||||
|
self.on_target_file = self.target.tempfile('energy', '.csv')
|
||||||
|
self.command = '{} -o {}'.format(self.binary, self.on_target_file)
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.binary = self.target.install(os.path.join(PACKAGE_BIN_DIRECTORY,
|
||||||
|
self.target.abi, self.binname))
|
||||||
|
|
||||||
|
def reset(self, sites=None, kinds=None):
|
||||||
|
super(JunoEnergyInstrument, self).reset(sites, kinds)
|
||||||
|
self.target.killall(self.binname, as_root=True)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.target.kick_off(self.command, as_root=True)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.target.killall(self.binname, signal='TERM', as_root=True)
|
||||||
|
|
||||||
|
def get_data(self, output_file):
|
||||||
|
temp_file = tempfile.mktemp()
|
||||||
|
self.target.pull(self.on_target_file, temp_file)
|
||||||
|
self.target.remove(self.on_target_file)
|
||||||
|
|
||||||
|
with open(temp_file, 'rb') as fh:
|
||||||
|
reader = csv.reader(fh)
|
||||||
|
headings = reader.next()
|
||||||
|
|
||||||
|
# Figure out which columns from the collected csv we actually want
|
||||||
|
select_columns = []
|
||||||
|
for chan in self.active_channels:
|
||||||
|
try:
|
||||||
|
select_columns.append(headings.index(chan.name))
|
||||||
|
except ValueError:
|
||||||
|
raise HostError('Channel "{}" is not in {}'.format(chan.name, temp_file))
|
||||||
|
|
||||||
|
with open(output_file, 'wb') as wfh:
|
||||||
|
write_headings = ['{}_{}'.format(c.site, c.kind)
|
||||||
|
for c in self.active_channels]
|
||||||
|
writer = csv.writer(wfh)
|
||||||
|
writer.writerow(write_headings)
|
||||||
|
for row in reader:
|
||||||
|
write_row = [row[c] for c in select_columns]
|
||||||
|
writer.writerow(write_row)
|
||||||
|
|
||||||
|
return MeasurementsCsv(output_file, self.active_channels)
|
||||||
|
|
||||||
|
|
980
devlib/target.py
Normal file
980
devlib/target.py
Normal file
@ -0,0 +1,980 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import posixpath
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from devlib.host import LocalConnection, PACKAGE_BIN_DIRECTORY
|
||||||
|
from devlib.module import get_module
|
||||||
|
from devlib.platform import Platform
|
||||||
|
from devlib.exception import TargetError, TargetNotRespondingError, TimeoutError
|
||||||
|
from devlib.utils.ssh import SshConnection
|
||||||
|
from devlib.utils.android import AdbConnection, AndroidProperties, adb_command, adb_disconnect
|
||||||
|
from devlib.utils.misc import memoized, isiterable, convert_new_lines, merge_lists
|
||||||
|
from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list, escape_double_quotes
|
||||||
|
from devlib.utils.types import integer, boolean, bitmask, identifier, caseless_string
|
||||||
|
|
||||||
|
|
||||||
|
FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (\S+) type (\S+) \((\S+)\)')
|
||||||
|
ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn)=([0-9]+|true|false)',
|
||||||
|
re.IGNORECASE)
|
||||||
|
ANDROID_SCREEN_RESOLUTION_REGEX = re.compile(r'mUnrestrictedScreen=\(\d+,\d+\)'
|
||||||
|
r'\s+(?P<width>\d+)x(?P<height>\d+)')
|
||||||
|
DEFAULT_SHELL_PROMPT = re.compile(r'^.*(shell|root)@.*:/\S* [#$] ',
|
||||||
|
re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
class Target(object):
|
||||||
|
|
||||||
|
conn_cls = None
|
||||||
|
path = None
|
||||||
|
os = None
|
||||||
|
|
||||||
|
default_modules = [
|
||||||
|
'hotplug',
|
||||||
|
'cpufreq',
|
||||||
|
'cpuidle',
|
||||||
|
'cgroups',
|
||||||
|
'hwmon',
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def core_names(self):
|
||||||
|
return self.platform.core_names
|
||||||
|
|
||||||
|
@property
|
||||||
|
def core_clusters(self):
|
||||||
|
return self.platform.core_clusters
|
||||||
|
|
||||||
|
@property
|
||||||
|
def big_core(self):
|
||||||
|
return self.platform.big_core
|
||||||
|
|
||||||
|
@property
|
||||||
|
def little_core(self):
|
||||||
|
return self.platform.little_core
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self):
|
||||||
|
return self.conn is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def connected_as_root(self):
|
||||||
|
result = self.execute('id')
|
||||||
|
return 'uid=0(' in result
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def is_rooted(self):
|
||||||
|
if self.connected_as_root:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
self.execute('ls /', timeout=2, as_root=True)
|
||||||
|
return True
|
||||||
|
except (TargetError, TimeoutError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def kernel_version(self):
|
||||||
|
return KernelVersion(self.execute('uname -r -v').strip())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def os_version(self): # pylint: disable=no-self-use
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def abi(self): # pylint: disable=no-self-use
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def cpuinfo(self):
|
||||||
|
return Cpuinfo(self.execute('cat /proc/cpuinfo'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def number_of_cpus(self):
|
||||||
|
num_cpus = 0
|
||||||
|
corere = re.compile(r'^\s*cpu\d+\s*$')
|
||||||
|
output = self.execute('ls /sys/devices/system/cpu')
|
||||||
|
for entry in output.split():
|
||||||
|
if corere.match(entry):
|
||||||
|
num_cpus += 1
|
||||||
|
return num_cpus
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def config(self):
|
||||||
|
try:
|
||||||
|
return KernelConfig(self.execute('zcat /proc/config.gz'))
|
||||||
|
except TargetError:
|
||||||
|
for path in ['/boot/config', '/boot/config-$(uname -r)']:
|
||||||
|
try:
|
||||||
|
return KernelConfig(self.execute('cat {}'.format(path)))
|
||||||
|
except TargetError:
|
||||||
|
pass
|
||||||
|
return KernelConfig('')
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def user(self):
|
||||||
|
return self.getenv('USER')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def conn(self):
|
||||||
|
if self._connections:
|
||||||
|
tid = id(threading.current_thread())
|
||||||
|
if tid not in self._connections:
|
||||||
|
self._connections[tid] = self.get_connection()
|
||||||
|
return self._connections[tid]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
connection_settings=None,
|
||||||
|
platform=None,
|
||||||
|
working_directory=None,
|
||||||
|
executables_directory=None,
|
||||||
|
connect=True,
|
||||||
|
modules=None,
|
||||||
|
load_default_modules=True,
|
||||||
|
shell_prompt=DEFAULT_SHELL_PROMPT,
|
||||||
|
):
|
||||||
|
self.connection_settings = connection_settings or {}
|
||||||
|
self.platform = platform or Platform()
|
||||||
|
self.working_directory = working_directory
|
||||||
|
self.executables_directory = executables_directory
|
||||||
|
self.modules = modules or []
|
||||||
|
self.load_default_modules = load_default_modules
|
||||||
|
self.shell_prompt = shell_prompt
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
self._installed_binaries = {}
|
||||||
|
self._installed_modules = {}
|
||||||
|
self._cache = {}
|
||||||
|
self._connections = {}
|
||||||
|
self.busybox = None
|
||||||
|
|
||||||
|
if load_default_modules:
|
||||||
|
module_lists = [self.default_modules]
|
||||||
|
else:
|
||||||
|
module_lists = []
|
||||||
|
module_lists += [self.modules, self.platform.modules]
|
||||||
|
self.modules = merge_lists(*module_lists, duplicates='first')
|
||||||
|
self._update_modules('early')
|
||||||
|
if connect:
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
# connection and initialization
|
||||||
|
|
||||||
|
def connect(self, timeout=None):
|
||||||
|
self.platform.init_target_connection(self)
|
||||||
|
tid = id(threading.current_thread())
|
||||||
|
self._connections[tid] = self.get_connection(timeout=timeout)
|
||||||
|
self.busybox = self.get_installed('busybox')
|
||||||
|
self._update_modules('connected')
|
||||||
|
self.platform.update_from_target(self)
|
||||||
|
if self.platform.big_core and self.load_default_modules:
|
||||||
|
self._install_module(get_module('bl'))
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
for conn in self._connections.itervalues():
|
||||||
|
conn.close()
|
||||||
|
self._connections = {}
|
||||||
|
|
||||||
|
def get_connection(self, timeout=None):
|
||||||
|
if self.conn_cls is None:
|
||||||
|
raise NotImplementedError('conn_cls must be set by the subclass of Target')
|
||||||
|
return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
|
||||||
|
|
||||||
|
def setup(self, executables=None):
|
||||||
|
self.execute('mkdir -p {}'.format(self.working_directory))
|
||||||
|
self.execute('mkdir -p {}'.format(self.executables_directory))
|
||||||
|
self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
|
||||||
|
for host_exe in (executables or []): # pylint: disable=superfluous-parens
|
||||||
|
self.install(host_exe)
|
||||||
|
|
||||||
|
def reboot(self, hard=False, connect=True, timeout=180):
|
||||||
|
if hard:
|
||||||
|
if not self.has('hard_reset'):
|
||||||
|
raise TargetError('Hard reset not supported for this target.')
|
||||||
|
self.hard_reset() # pylint: disable=no-member
|
||||||
|
else:
|
||||||
|
if not self.is_connected:
|
||||||
|
message = 'Cannot reboot target becuase it is disconnected. ' +\
|
||||||
|
'Either connect() first, or specify hard=True ' +\
|
||||||
|
'(in which case, a hard_reset module must be installed)'
|
||||||
|
raise TargetError(message)
|
||||||
|
self.reset()
|
||||||
|
if self.has('boot'):
|
||||||
|
self.boot() # pylint: disable=no-member
|
||||||
|
if connect:
|
||||||
|
self.connect(timeout=timeout)
|
||||||
|
|
||||||
|
# file transfer
|
||||||
|
|
||||||
|
def push(self, source, dest, timeout=None):
|
||||||
|
return self.conn.push(source, dest, timeout=timeout)
|
||||||
|
|
||||||
|
def pull(self, source, dest, timeout=None):
|
||||||
|
return self.conn.pull(source, dest, timeout=timeout)
|
||||||
|
|
||||||
|
# execution
|
||||||
|
|
||||||
|
def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
|
||||||
|
return self.conn.execute(command, timeout, check_exit_code, as_root)
|
||||||
|
|
||||||
|
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
|
||||||
|
return self.conn.background(command, stdout, stderr, as_root)
|
||||||
|
|
||||||
|
def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
|
||||||
|
as_root=False, timeout=30):
|
||||||
|
"""
|
||||||
|
Executes the specified binary under the specified conditions.
|
||||||
|
|
||||||
|
:binary: binary to execute. Must be present and executable on the device.
|
||||||
|
:args: arguments to be passed to the binary. The can be either a list or
|
||||||
|
a string.
|
||||||
|
:in_directory: execute the binary in the specified directory. This must
|
||||||
|
be an absolute path.
|
||||||
|
:on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
|
||||||
|
case, it will be interpreted as the mask), a list of ``ints``, in which
|
||||||
|
case this will be interpreted as the list of cpus, or string, which
|
||||||
|
will be interpreted as a comma-separated list of cpu ranges, e.g.
|
||||||
|
``"0,4-7"``.
|
||||||
|
:as_root: Specify whether the command should be run as root
|
||||||
|
:timeout: If the invocation does not terminate within this number of seconds,
|
||||||
|
a ``TimeoutError`` exception will be raised. Set to ``None`` if the
|
||||||
|
invocation should not timeout.
|
||||||
|
|
||||||
|
"""
|
||||||
|
command = binary
|
||||||
|
if args:
|
||||||
|
if isiterable(args):
|
||||||
|
args = ' '.join(args)
|
||||||
|
command = '{} {}'.format(command, args)
|
||||||
|
if on_cpus:
|
||||||
|
on_cpus = bitmask(on_cpus)
|
||||||
|
command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
|
||||||
|
if in_directory:
|
||||||
|
command = 'cd {} && {}'.format(in_directory, command)
|
||||||
|
return self.execute(command, as_root=as_root, timeout=timeout)
|
||||||
|
|
||||||
|
def kick_off(self, command, as_root=False):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
# sysfs interaction
|
||||||
|
|
||||||
|
def read_value(self, path, kind=None):
|
||||||
|
output = self.execute('cat \'{}\''.format(path), as_root=self.is_rooted).strip() # pylint: disable=E1103
|
||||||
|
if kind:
|
||||||
|
return kind(output)
|
||||||
|
else:
|
||||||
|
return output
|
||||||
|
|
||||||
|
def read_int(self, path):
|
||||||
|
return self.read_value(path, kind=integer)
|
||||||
|
|
||||||
|
def read_bool(self, path):
|
||||||
|
return self.read_value(path, kind=boolean)
|
||||||
|
|
||||||
|
def write_value(self, path, value, verify=True):
|
||||||
|
value = str(value)
|
||||||
|
self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True)
|
||||||
|
if verify:
|
||||||
|
output = self.read_value(path)
|
||||||
|
if not output == value:
|
||||||
|
message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
|
||||||
|
raise TargetError(message)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
try:
|
||||||
|
self.execute('reboot', as_root=self.is_rooted, timeout=2)
|
||||||
|
except (TargetError, TimeoutError, subprocess.CalledProcessError):
|
||||||
|
# on some targets "reboot" doesn't return gracefully
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_responsive(self):
|
||||||
|
try:
|
||||||
|
self.conn.execute('ls /', timeout=5)
|
||||||
|
except (TimeoutError, subprocess.CalledProcessError):
|
||||||
|
raise TargetNotRespondingError(self.conn.name)
|
||||||
|
|
||||||
|
# process management
|
||||||
|
|
||||||
|
def kill(self, pid, signal=None, as_root=False):
|
||||||
|
signal_string = '-s {}'.format(signal) if signal else ''
|
||||||
|
self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
|
||||||
|
|
||||||
|
def killall(self, process_name, signal=None, as_root=False):
|
||||||
|
for pid in self.get_pids_of(process_name):
|
||||||
|
self.kill(pid, signal=signal, as_root=as_root)
|
||||||
|
|
||||||
|
def get_pids_of(self, process_name):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def ps(self, **kwargs):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
# files
|
||||||
|
|
||||||
|
def file_exists(self, filepath):
|
||||||
|
command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
|
||||||
|
return boolean(self.execute(command.format(filepath)).strip())
|
||||||
|
|
||||||
|
def list_file_systems(self):
|
||||||
|
output = self.execute('mount')
|
||||||
|
fstab = []
|
||||||
|
for line in output.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
match = FSTAB_ENTRY_REGEX.search(line)
|
||||||
|
if match:
|
||||||
|
fstab.append(FstabEntry(match.group(1), match.group(2),
|
||||||
|
match.group(3), match.group(4),
|
||||||
|
None, None))
|
||||||
|
else: # assume pre-M Android
|
||||||
|
fstab.append(FstabEntry(*line.split()))
|
||||||
|
return fstab
|
||||||
|
|
||||||
|
def list_directory(self, path, as_root=False):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_workpath(self, name):
|
||||||
|
return self.path.join(self.working_directory, name)
|
||||||
|
|
||||||
|
def tempfile(self, prefix='', suffix=''):
|
||||||
|
names = tempfile._get_candidate_names() # pylint: disable=W0212
|
||||||
|
for _ in xrange(tempfile.TMP_MAX):
|
||||||
|
name = names.next()
|
||||||
|
path = self.get_workpath(prefix + name + suffix)
|
||||||
|
if not self.file_exists(path):
|
||||||
|
return path
|
||||||
|
raise IOError('No usable temporary filename found')
|
||||||
|
|
||||||
|
def remove(self, path, as_root=False):
|
||||||
|
self.execute('rm -rf {}'.format(path), as_root=as_root)
|
||||||
|
|
||||||
|
# misc
|
||||||
|
def core_cpus(self, core):
|
||||||
|
return [i for i, c in enumerate(self.core_names) if c == core]
|
||||||
|
|
||||||
|
def list_online_cpus(self, core=None):
|
||||||
|
path = self.path.join('/sys/devices/system/cpu/online')
|
||||||
|
output = self.read_value(path)
|
||||||
|
all_online = ranges_to_list(output)
|
||||||
|
if core:
|
||||||
|
cpus = self.core_cpus(core)
|
||||||
|
if not cpus:
|
||||||
|
raise ValueError(core)
|
||||||
|
return [o for o in all_online if o in cpus]
|
||||||
|
else:
|
||||||
|
return all_online
|
||||||
|
|
||||||
|
def list_offline_cpus(self):
|
||||||
|
online = self.list_online_cpus()
|
||||||
|
return [c for c in xrange(self.number_of_cpus)
|
||||||
|
if c not in online]
|
||||||
|
|
||||||
|
def getenv(self, variable):
|
||||||
|
return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
|
||||||
|
|
||||||
|
def capture_screen(self, filepath):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def install(self, filepath, timeout=None, with_name=None):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def uninstall(self, name):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_installed(self, name):
|
||||||
|
for path in self.getenv('PATH').split(self.path.pathsep):
|
||||||
|
try:
|
||||||
|
if name in self.list_directory(path):
|
||||||
|
return self.path.join(path, name)
|
||||||
|
except TargetError:
|
||||||
|
pass # directory does not exist or no executable premssions
|
||||||
|
if name in self.list_directory(self.executables_directory):
|
||||||
|
return self.path.join(self.executables_directory, name)
|
||||||
|
|
||||||
|
which = get_installed
|
||||||
|
|
||||||
|
def is_installed(self, name):
|
||||||
|
return bool(self.get_installed(name))
|
||||||
|
|
||||||
|
def bin(self, name):
|
||||||
|
return self._installed_binaries.get(name, name)
|
||||||
|
|
||||||
|
def has(self, modname):
|
||||||
|
return hasattr(self, identifier(modname))
|
||||||
|
|
||||||
|
def _update_modules(self, stage):
|
||||||
|
for mod in self.modules:
|
||||||
|
if isinstance(mod, dict):
|
||||||
|
mod, params = mod.items()[0]
|
||||||
|
else:
|
||||||
|
params = {}
|
||||||
|
mod = get_module(mod)
|
||||||
|
if not mod.stage == stage:
|
||||||
|
continue
|
||||||
|
if mod.probe(self):
|
||||||
|
self._install_module(mod, **params)
|
||||||
|
else:
|
||||||
|
self.logger.debug('Module {} is not supported by the target'.format(mod.name))
|
||||||
|
|
||||||
|
def _install_module(self, mod, **params):
|
||||||
|
if mod.name not in self._installed_modules:
|
||||||
|
self.logger.debug('Installing module {}'.format(mod.name))
|
||||||
|
mod.install(self, **params)
|
||||||
|
self._installed_modules[mod.name] = mod
|
||||||
|
else:
|
||||||
|
self.logger.debug('Module {} is already installed.'.format(mod.name))
|
||||||
|
|
||||||
|
|
||||||
|
class LinuxTarget(Target):
|
||||||
|
|
||||||
|
conn_cls = SshConnection
|
||||||
|
path = posixpath
|
||||||
|
os = 'linux'
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def abi(self):
|
||||||
|
value = self.execute('uname -m').strip()
|
||||||
|
for abi, architectures in ABI_MAP.iteritems():
|
||||||
|
if value in architectures:
|
||||||
|
result = abi
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
result = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def os_version(self):
|
||||||
|
os_version = {}
|
||||||
|
try:
|
||||||
|
command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
|
||||||
|
version_files = self.execute(command, check_exit_code=False).strip().split()
|
||||||
|
for vf in version_files:
|
||||||
|
name = self.path.basename(vf)
|
||||||
|
output = self.read_value(vf)
|
||||||
|
os_version[name] = output.strip().replace('\n', ' ')
|
||||||
|
except TargetError:
|
||||||
|
raise
|
||||||
|
return os_version
|
||||||
|
|
||||||
|
def connect(self, timeout=None):
|
||||||
|
super(LinuxTarget, self).connect(timeout=timeout)
|
||||||
|
if self.working_directory is None:
|
||||||
|
if self.connected_as_root:
|
||||||
|
self.working_directory = '/root/devlib-target'
|
||||||
|
else:
|
||||||
|
self.working_directory = '/home/{}/devlib-target'.format(self.user)
|
||||||
|
if self.executables_directory is None:
|
||||||
|
self.executables_directory = self.path.join(self.working_directory, 'bin')
|
||||||
|
|
||||||
|
def kick_off(self, command, as_root=False):
|
||||||
|
command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
|
||||||
|
return self.conn.execute(command, as_root=as_root)
|
||||||
|
|
||||||
|
def get_pids_of(self, process_name):
|
||||||
|
"""Returns a list of PIDs of all processes with the specified name."""
|
||||||
|
# result should be a column of PIDs with the first row as "PID" header
|
||||||
|
result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
|
||||||
|
check_exit_code=False).strip().split()
|
||||||
|
if len(result) >= 2: # at least one row besides the header
|
||||||
|
return map(int, result[1:])
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def ps(self, **kwargs):
|
||||||
|
command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
|
||||||
|
lines = iter(convert_new_lines(self.execute(command)).split('\n'))
|
||||||
|
lines.next() # header
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for line in lines:
|
||||||
|
parts = re.split(r'\s+', line, maxsplit=8)
|
||||||
|
if parts and parts != ['']:
|
||||||
|
result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
|
||||||
|
|
||||||
|
if not kwargs:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
filtered_result = []
|
||||||
|
for entry in result:
|
||||||
|
if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
|
||||||
|
filtered_result.append(entry)
|
||||||
|
return filtered_result
|
||||||
|
|
||||||
|
def list_directory(self, path, as_root=False):
|
||||||
|
contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
|
||||||
|
return [x.strip() for x in contents.split('\n') if x.strip()]
|
||||||
|
|
||||||
|
def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
|
||||||
|
destpath = self.path.join(self.executables_directory,
|
||||||
|
with_name and with_name or self.path.basename(filepath))
|
||||||
|
self.push(filepath, destpath)
|
||||||
|
self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
|
||||||
|
self._installed_binaries[self.path.basename(destpath)] = destpath
|
||||||
|
return destpath
|
||||||
|
|
||||||
|
def uninstall(self, name):
|
||||||
|
path = self.path.join(self.executables_directory, name)
|
||||||
|
self.remove(path)
|
||||||
|
|
||||||
|
def capture_screen(self, filepath):
|
||||||
|
if not self.is_installed('scrot'):
|
||||||
|
self.logger.debug('Could not take screenshot as scrot is not installed.')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
|
||||||
|
tmpfile = self.tempfile()
|
||||||
|
self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
|
||||||
|
self.pull(tmpfile, filepath)
|
||||||
|
self.remove(tmpfile)
|
||||||
|
except TargetError as e:
|
||||||
|
if "Can't open X dispay." not in e.message:
|
||||||
|
raise e
|
||||||
|
message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
|
||||||
|
self.logger.debug('Could not take screenshot: {}'.format(message))
|
||||||
|
|
||||||
|
|
||||||
|
class AndroidTarget(Target):
|
||||||
|
|
||||||
|
conn_cls = AdbConnection
|
||||||
|
path = posixpath
|
||||||
|
os = 'android'
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def abi(self):
|
||||||
|
return self.getprop()['ro.product.cpu.abi'].split('-')[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def os_version(self):
|
||||||
|
os_version = {}
|
||||||
|
for k, v in self.getprop().iteritems():
|
||||||
|
if k.startswith('ro.build.version'):
|
||||||
|
part = k.split('.')[-1]
|
||||||
|
os_version[part] = v
|
||||||
|
return os_version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adb_name(self):
|
||||||
|
return self.conn.device
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def screen_resolution(self):
|
||||||
|
output = self.execute('dumpsys window')
|
||||||
|
match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
|
||||||
|
if match:
|
||||||
|
return (int(match.group('width')),
|
||||||
|
int(match.group('height')))
|
||||||
|
else:
|
||||||
|
return (0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(AndroidTarget, self).__init__(*args, **kwargs)
|
||||||
|
self._file_transfer_cache = None
|
||||||
|
|
||||||
|
def reset(self, fastboot=False): # pylint: disable=arguments-differ
|
||||||
|
try:
|
||||||
|
self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
|
||||||
|
as_root=self.is_rooted, timeout=2)
|
||||||
|
except (TargetError, TimeoutError, subprocess.CalledProcessError):
|
||||||
|
# on some targets "reboot" doesn't return gracefully
|
||||||
|
pass
|
||||||
|
|
||||||
|
def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
|
||||||
|
start = time.time()
|
||||||
|
device = self.connection_settings.get('device')
|
||||||
|
if device and ':' in device:
|
||||||
|
# ADB does not automatically remove a network device from it's
|
||||||
|
# devices list when the connection is broken by the remote, so the
|
||||||
|
# adb connection may have gone "stale", resulting in adb blocking
|
||||||
|
# indefinitely when making calls to the device. To avoid this,
|
||||||
|
# always disconnect first.
|
||||||
|
adb_disconnect(device)
|
||||||
|
super(AndroidTarget, self).connect(timeout=timeout)
|
||||||
|
if self.working_directory is None:
|
||||||
|
self.working_directory = '/data/local/tmp/devlib-target'
|
||||||
|
self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
|
||||||
|
if self.executables_directory is None:
|
||||||
|
self.executables_directory = self.path.join(self.working_directory, 'bin')
|
||||||
|
|
||||||
|
if check_boot_completed:
|
||||||
|
boot_completed = boolean(self.getprop('sys.boot_completed'))
|
||||||
|
while not boot_completed and timeout >= time.time() - start:
|
||||||
|
time.sleep(5)
|
||||||
|
boot_completed = boolean(self.getprop('sys.boot_completed'))
|
||||||
|
if not boot_completed:
|
||||||
|
raise TargetError('Connected but Android did not fully boot.')
|
||||||
|
|
||||||
|
def setup(self, executables=None):
|
||||||
|
super(AndroidTarget, self).setup(executables)
|
||||||
|
self.execute('mkdir -p {}'.format(self._file_transfer_cache))
|
||||||
|
|
||||||
|
def kick_off(self, command, as_root=False):
|
||||||
|
"""
|
||||||
|
Like execute but closes adb session and returns immediately, leaving the command running on the
|
||||||
|
device (this is different from execute(background=True) which keeps adb connection open and returns
|
||||||
|
a subprocess object).
|
||||||
|
|
||||||
|
.. note:: This relies on busybox's nohup applet and so won't work on unrooted devices.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.is_rooted:
|
||||||
|
raise TargetError('kick_off uses busybox\'s nohup applet and so can only be run a rooted device.')
|
||||||
|
try:
|
||||||
|
command = 'cd {} && {} nohup {}'.format(self.working_directory, self.bin('busybox'), command)
|
||||||
|
output = self.execute(command, timeout=1, as_root=as_root)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValueError('Background command exited before timeout; got "{}"'.format(output))
|
||||||
|
|
||||||
|
def list_directory(self, path, as_root=False):
|
||||||
|
contents = self.execute('ls {}'.format(path), as_root=as_root)
|
||||||
|
return [x.strip() for x in contents.split('\n') if x.strip()]
|
||||||
|
|
||||||
|
def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
|
||||||
|
ext = os.path.splitext(filepath)[1].lower()
|
||||||
|
if ext == '.apk':
|
||||||
|
return self.install_apk(filepath, timeout)
|
||||||
|
else:
|
||||||
|
return self.install_executable(filepath, with_name)
|
||||||
|
|
||||||
|
def uninstall(self, name):
|
||||||
|
if self.package_is_installed(name):
|
||||||
|
self.uninstall_package(name)
|
||||||
|
else:
|
||||||
|
self.uninstall_executable(name)
|
||||||
|
|
||||||
|
def get_pids_of(self, process_name):
|
||||||
|
result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
|
||||||
|
if result and 'not found' not in result:
|
||||||
|
return [int(x.split()[1]) for x in result.split('\n')[1:]]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def ps(self, **kwargs):
|
||||||
|
lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
|
||||||
|
lines.next() # header
|
||||||
|
result = []
|
||||||
|
for line in lines:
|
||||||
|
parts = line.split()
|
||||||
|
if parts:
|
||||||
|
result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
|
||||||
|
if not kwargs:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
filtered_result = []
|
||||||
|
for entry in result:
|
||||||
|
if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
|
||||||
|
filtered_result.append(entry)
|
||||||
|
return filtered_result
|
||||||
|
|
||||||
|
def capture_screen(self, filepath):
|
||||||
|
on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
|
||||||
|
self.execute('screencap -p {}'.format(on_device_file))
|
||||||
|
self.pull(on_device_file, filepath)
|
||||||
|
self.remove(on_device_file)
|
||||||
|
|
||||||
|
def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
|
||||||
|
if not as_root:
|
||||||
|
self.conn.push(source, dest, timeout=timeout)
|
||||||
|
else:
|
||||||
|
device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
|
||||||
|
self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile)))
|
||||||
|
self.conn.push(source, device_tempfile, timeout=timeout)
|
||||||
|
self.execute('cp {} {}'.format(device_tempfile, dest), as_root=True)
|
||||||
|
|
||||||
|
def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
|
||||||
|
if not as_root:
|
||||||
|
self.conn.pull(source, dest, timeout=timeout)
|
||||||
|
else:
|
||||||
|
device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
|
||||||
|
self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile)))
|
||||||
|
self.execute('cp {} {}'.format(source, device_tempfile), as_root=True)
|
||||||
|
self.conn.pull(device_tempfile, dest, timeout=timeout)
|
||||||
|
|
||||||
|
# Android-specific
|
||||||
|
|
||||||
|
def swipe_to_unlock(self):
|
||||||
|
width, height = self.screen_resolution
|
||||||
|
swipe_heigh = height * 2 // 3
|
||||||
|
start = 100
|
||||||
|
stop = width - start
|
||||||
|
command = 'input swipe {} {} {} {}'
|
||||||
|
self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
|
||||||
|
|
||||||
|
def getprop(self, prop=None):
|
||||||
|
props = AndroidProperties(self.execute('getprop'))
|
||||||
|
if prop:
|
||||||
|
return props[prop]
|
||||||
|
return props
|
||||||
|
|
||||||
|
def is_installed(self, name):
|
||||||
|
return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
|
||||||
|
|
||||||
|
def package_is_installed(self, package_name):
|
||||||
|
return package_name in self.list_packages()
|
||||||
|
|
||||||
|
def list_packages(self):
|
||||||
|
output = self.execute('pm list packages')
|
||||||
|
output = output.replace('package:', '')
|
||||||
|
return output.split()
|
||||||
|
|
||||||
|
def get_package_version(self, package):
|
||||||
|
output = self.execute('dumpsys package {}'.format(package))
|
||||||
|
for line in convert_new_lines(output).split('\n'):
|
||||||
|
if 'versionName' in line:
|
||||||
|
return line.split('=', 1)[1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def install_apk(self, filepath, timeout=None): # pylint: disable=W0221
|
||||||
|
ext = os.path.splitext(filepath)[1].lower()
|
||||||
|
if ext == '.apk':
|
||||||
|
return adb_command(self.adb_name, "install {}".format(filepath), timeout=timeout)
|
||||||
|
else:
|
||||||
|
raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
|
||||||
|
|
||||||
|
def install_executable(self, filepath, with_name=None):
|
||||||
|
self._ensure_executables_directory_is_writable()
|
||||||
|
executable_name = with_name or os.path.basename(filepath)
|
||||||
|
on_device_file = self.path.join(self.working_directory, executable_name)
|
||||||
|
on_device_executable = self.path.join(self.executables_directory, executable_name)
|
||||||
|
self.push(filepath, on_device_file)
|
||||||
|
if on_device_file != on_device_executable:
|
||||||
|
self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.is_rooted)
|
||||||
|
self.remove(on_device_file, as_root=self.is_rooted)
|
||||||
|
self.execute('chmod 0777 {}'.format(on_device_executable), as_root=self.is_rooted)
|
||||||
|
self._installed_binaries[executable_name] = on_device_executable
|
||||||
|
return on_device_executable
|
||||||
|
|
||||||
|
def uninstall_package(self, package):
|
||||||
|
adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
|
||||||
|
|
||||||
|
def uninstall_executable(self, executable_name):
|
||||||
|
on_device_executable = self.path.join(self.executables_directory, executable_name)
|
||||||
|
self._ensure_executables_directory_is_writable()
|
||||||
|
self.remove(on_device_executable, as_root=self.is_rooted)
|
||||||
|
|
||||||
|
def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
|
||||||
|
op = '>>' if append == True else '>'
|
||||||
|
filtstr = ' -s {}'.format(filter) if filter else ''
|
||||||
|
command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
|
||||||
|
adb_command(self.adb_name, command, timeout=timeout)
|
||||||
|
|
||||||
|
def clear_logcat(self):
|
||||||
|
adb_command(self.adb_name, 'logcat -c', timeout=30)
|
||||||
|
|
||||||
|
def is_screen_on(self):
|
||||||
|
output = self.execute('dumpsys power')
|
||||||
|
match = ANDROID_SCREEN_STATE_REGEX.search(output)
|
||||||
|
if match:
|
||||||
|
return boolean(match.group(1))
|
||||||
|
else:
|
||||||
|
raise TargetError('Could not establish screen state.')
|
||||||
|
|
||||||
|
def ensure_screen_is_on(self):
|
||||||
|
if not self.is_screen_on():
|
||||||
|
self.execute('input keyevent 26')
|
||||||
|
|
||||||
|
def _ensure_executables_directory_is_writable(self):
|
||||||
|
matched = []
|
||||||
|
for entry in self.list_file_systems():
|
||||||
|
if self.executables_directory.rstrip('/').startswith(entry.mount_point):
|
||||||
|
matched.append(entry)
|
||||||
|
if matched:
|
||||||
|
entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
|
||||||
|
if 'rw' not in entry.options:
|
||||||
|
self.execute('mount -o rw,remount {} {}'.format(entry.device,
|
||||||
|
entry.mount_point),
|
||||||
|
as_root=True)
|
||||||
|
else:
|
||||||
|
message = 'Could not find mount point for executables directory {}'
|
||||||
|
raise TargetError(message.format(self.executables_directory))
|
||||||
|
|
||||||
|
|
||||||
|
FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
|
||||||
|
PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
|
||||||
|
|
||||||
|
|
||||||
|
class Cpuinfo(object):
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def architecture(self):
|
||||||
|
for section in self.sections:
|
||||||
|
if 'CPU architecture' in section:
|
||||||
|
return section['CPU architecture']
|
||||||
|
if 'architecture' in section:
|
||||||
|
return section['architecture']
|
||||||
|
|
||||||
|
@property
|
||||||
|
@memoized
|
||||||
|
def cpu_names(self):
|
||||||
|
cpu_names = []
|
||||||
|
global_name = None
|
||||||
|
for section in self.sections:
|
||||||
|
if 'processor' in section:
|
||||||
|
if 'CPU part' in section:
|
||||||
|
cpu_names.append(_get_part_name(section))
|
||||||
|
elif 'model name' in section:
|
||||||
|
cpu_names.append(_get_model_name(section))
|
||||||
|
else:
|
||||||
|
cpu_names.append(None)
|
||||||
|
elif 'CPU part' in section:
|
||||||
|
global_name = _get_part_name(section)
|
||||||
|
return [caseless_string(c or global_name) for c in cpu_names]
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
self.sections = None
|
||||||
|
self.text = None
|
||||||
|
self.parse(text)
|
||||||
|
|
||||||
|
@memoized
|
||||||
|
def get_cpu_features(self, cpuid=0):
|
||||||
|
global_features = []
|
||||||
|
for section in self.sections:
|
||||||
|
if 'processor' in section:
|
||||||
|
if int(section.get('processor')) != cpuid:
|
||||||
|
continue
|
||||||
|
if 'Features' in section:
|
||||||
|
return section.get('Features').split()
|
||||||
|
elif 'Features' in section:
|
||||||
|
global_features = section.get('Features').split()
|
||||||
|
return global_features
|
||||||
|
|
||||||
|
def parse(self, text):
|
||||||
|
self.sections = []
|
||||||
|
current_section = {}
|
||||||
|
self.text = text.strip()
|
||||||
|
for line in self.text.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
key, value = line.split(':', 1)
|
||||||
|
current_section[key.strip()] = value.strip()
|
||||||
|
else: # not line
|
||||||
|
self.sections.append(current_section)
|
||||||
|
current_section = {}
|
||||||
|
self.sections.append(current_section)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'CpuInfo({})'.format(self.cpu_names)
|
||||||
|
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
|
||||||
|
class KernelVersion(object):
|
||||||
|
|
||||||
|
def __init__(self, version_string):
|
||||||
|
if ' #' in version_string:
|
||||||
|
release, version = version_string.split(' #')
|
||||||
|
self.release = release
|
||||||
|
self.version = version
|
||||||
|
elif version_string.startswith('#'):
|
||||||
|
self.release = ''
|
||||||
|
self.version = version_string
|
||||||
|
else:
|
||||||
|
self.release = version_string
|
||||||
|
self.version = ''
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{} {}'.format(self.release, self.version)
|
||||||
|
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
|
||||||
|
class KernelConfig(object):
|
||||||
|
|
||||||
|
not_set_regex = re.compile(r'# (\S+) is not set')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_config_name(name):
|
||||||
|
name = name.upper()
|
||||||
|
if not name.startswith('CONFIG_'):
|
||||||
|
name = 'CONFIG_' + name
|
||||||
|
return name
|
||||||
|
|
||||||
|
def iteritems(self):
|
||||||
|
return self._config.iteritems()
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
self.text = text
|
||||||
|
self._config = {}
|
||||||
|
for line in text.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('#'):
|
||||||
|
match = self.not_set_regex.search(line)
|
||||||
|
if match:
|
||||||
|
self._config[match.group(1)] = 'n'
|
||||||
|
elif '=' in line:
|
||||||
|
name, value = line.split('=', 1)
|
||||||
|
self._config[name.strip()] = value.strip()
|
||||||
|
|
||||||
|
def get(self, name):
|
||||||
|
return self._config.get(self.get_config_name(name))
|
||||||
|
|
||||||
|
def like(self, name):
|
||||||
|
regex = re.compile(name, re.I)
|
||||||
|
result = {}
|
||||||
|
for k, v in self._config.iteritems():
|
||||||
|
if regex.search(k):
|
||||||
|
result[k] = v
|
||||||
|
return result
|
||||||
|
|
||||||
|
def is_enabled(self, name):
|
||||||
|
return self.get(name) == 'y'
|
||||||
|
|
||||||
|
def is_module(self, name):
|
||||||
|
return self.get(name) == 'm'
|
||||||
|
|
||||||
|
def is_not_set(self, name):
|
||||||
|
return self.get(name) == 'n'
|
||||||
|
|
||||||
|
def has(self, name):
|
||||||
|
return self.get(name) in ['m', 'y']
|
||||||
|
|
||||||
|
|
||||||
|
class LocalLinuxTarget(LinuxTarget):
|
||||||
|
|
||||||
|
conn_cls = LocalConnection
|
||||||
|
|
||||||
|
def connect(self, timeout=None):
|
||||||
|
if self.working_directory is None:
|
||||||
|
self.working_directory = '/tmp'
|
||||||
|
if self.executables_directory is None:
|
||||||
|
self.executables_directory = '/tmp'
|
||||||
|
super(LocalLinuxTarget, self).connect(timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_model_name(section):
|
||||||
|
name_string = section['model name']
|
||||||
|
parts = name_string.split('@')[0].strip().split()
|
||||||
|
return ' '.join([p for p in parts
|
||||||
|
if '(' not in p and p != 'CPU'])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_part_name(section):
|
||||||
|
implementer = section.get('CPU implementer', '0x0')
|
||||||
|
part = section['CPU part']
|
||||||
|
variant = section.get('CPU variant', '0x0')
|
||||||
|
name = get_cpu_name(*map(integer, [implementer, part, variant]))
|
||||||
|
if name is None:
|
||||||
|
name = '{}/{}/{}'.format(implementer, part, variant)
|
||||||
|
return name
|
||||||
|
|
20
devlib/trace/__init__.py
Normal file
20
devlib/trace/__init__.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class TraceCollector(object):
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
self.target = target
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_trace(self, outfile):
|
||||||
|
pass
|
199
devlib/trace/ftrace.py
Normal file
199
devlib/trace/ftrace.py
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
# Copyright 2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from devlib.trace import TraceCollector
|
||||||
|
from devlib.host import PACKAGE_BIN_DIRECTORY
|
||||||
|
from devlib.exception import TargetError, HostError
|
||||||
|
from devlib.utils.misc import check_output, which
|
||||||
|
|
||||||
|
|
||||||
|
TRACE_MARKER_START = 'TRACE_MARKER_START'
|
||||||
|
TRACE_MARKER_STOP = 'TRACE_MARKER_STOP'
|
||||||
|
OUTPUT_TRACE_FILE = 'trace.dat'
|
||||||
|
DEFAULT_EVENTS = [
|
||||||
|
'cpu_frequency',
|
||||||
|
'cpu_idle',
|
||||||
|
'sched_migrate_task',
|
||||||
|
'sched_process_exec',
|
||||||
|
'sched_process_fork',
|
||||||
|
'sched_stat_iowait',
|
||||||
|
'sched_switch',
|
||||||
|
'sched_wakeup',
|
||||||
|
'sched_wakeup_new',
|
||||||
|
]
|
||||||
|
TIMEOUT = 180
|
||||||
|
|
||||||
|
|
||||||
|
class FtraceCollector(TraceCollector):
|
||||||
|
|
||||||
|
def __init__(self, target,
|
||||||
|
events=None,
|
||||||
|
buffer_size=None,
|
||||||
|
buffer_size_step=1000,
|
||||||
|
buffer_size_file='/sys/kernel/debug/tracing/buffer_size_kb',
|
||||||
|
marker_file='/sys/kernel/debug/tracing/trace_marker',
|
||||||
|
automark=True,
|
||||||
|
autoreport=True,
|
||||||
|
autoview=False,
|
||||||
|
no_install=False,
|
||||||
|
):
|
||||||
|
super(FtraceCollector, self).__init__(target)
|
||||||
|
self.events = events if events is not None else DEFAULT_EVENTS
|
||||||
|
self.buffer_size = buffer_size
|
||||||
|
self.buffer_size_step = buffer_size_step
|
||||||
|
self.buffer_size_file = buffer_size_file
|
||||||
|
self.marker_file = marker_file
|
||||||
|
self.automark = automark
|
||||||
|
self.autoreport = autoreport
|
||||||
|
self.autoview = autoview
|
||||||
|
self.target_output_file = os.path.join(self.target.working_directory, OUTPUT_TRACE_FILE)
|
||||||
|
self.target_binary = None
|
||||||
|
self.host_binary = None
|
||||||
|
self.start_time = None
|
||||||
|
self.stop_time = None
|
||||||
|
self.event_string = _build_trace_events(self.events)
|
||||||
|
self._reset_needed = True
|
||||||
|
|
||||||
|
self.host_binary = which('trace-cmd')
|
||||||
|
self.kernelshark = which('kernelshark')
|
||||||
|
|
||||||
|
if not self.target.is_rooted:
|
||||||
|
raise TargetError('trace-cmd instrument cannot be used on an unrooted device.')
|
||||||
|
if self.autoreport and self.host_binary is None:
|
||||||
|
raise HostError('trace-cmd binary must be installed on the host if autoreport=True.')
|
||||||
|
if self.autoview and self.kernelshark is None:
|
||||||
|
raise HostError('kernelshark binary must be installed on the host if autoview=True.')
|
||||||
|
if not no_install:
|
||||||
|
host_file = os.path.join(PACKAGE_BIN_DIRECTORY, self.target.abi, 'trace-cmd')
|
||||||
|
self.target_binary = self.target.install(host_file)
|
||||||
|
else:
|
||||||
|
if not self.target.is_installed('trace-cmd'):
|
||||||
|
raise TargetError('No trace-cmd found on device and no_install=True is specified.')
|
||||||
|
self.target_binary = 'trace-cmd'
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
if self.buffer_size:
|
||||||
|
self._set_buffer_size()
|
||||||
|
self.target.execute('{} reset'.format(self.target_binary), as_root=True, timeout=TIMEOUT)
|
||||||
|
self._reset_needed = False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.start_time = time.time()
|
||||||
|
if self._reset_needed:
|
||||||
|
self.reset()
|
||||||
|
if self.automark:
|
||||||
|
self.mark_start()
|
||||||
|
self.target.execute('{} start {}'.format(self.target_binary, self.event_string), as_root=True)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.stop_time = time.time()
|
||||||
|
if self.automark:
|
||||||
|
self.mark_stop()
|
||||||
|
self.target.execute('{} stop'.format(self.target_binary), timeout=TIMEOUT, as_root=True)
|
||||||
|
self._reset_needed = True
|
||||||
|
|
||||||
|
def get_trace(self, outfile):
|
||||||
|
if os.path.isdir(outfile):
|
||||||
|
outfile = os.path.join(outfile, os.path.dirname(self.target_output_file))
|
||||||
|
self.target.execute('{} extract -o {}'.format(self.target_binary, self.target_output_file),
|
||||||
|
timeout=TIMEOUT, as_root=True)
|
||||||
|
|
||||||
|
# The size of trace.dat will depend on how long trace-cmd was running.
|
||||||
|
# Therefore timout for the pull command must also be adjusted
|
||||||
|
# accordingly.
|
||||||
|
pull_timeout = self.stop_time - self.start_time
|
||||||
|
self.target.pull(self.target_output_file, outfile, timeout=pull_timeout)
|
||||||
|
if not os.path.isfile(outfile):
|
||||||
|
self.logger.warning('Binary trace not pulled from device.')
|
||||||
|
else:
|
||||||
|
if self.autoreport:
|
||||||
|
textfile = os.path.splitext(outfile)[0] + '.txt'
|
||||||
|
self.report(outfile, textfile)
|
||||||
|
if self.autoview:
|
||||||
|
self.view(outfile)
|
||||||
|
|
||||||
|
def report(self, binfile, destfile):
|
||||||
|
# To get the output of trace.dat, trace-cmd must be installed
|
||||||
|
# This is done host-side because the generated file is very large
|
||||||
|
try:
|
||||||
|
command = '{} report {} > {}'.format(self.host_binary, binfile, destfile)
|
||||||
|
self.logger.debug(command)
|
||||||
|
process = subprocess.Popen(command, stderr=subprocess.PIPE, shell=True)
|
||||||
|
_, error = process.communicate()
|
||||||
|
if process.returncode:
|
||||||
|
raise TargetError('trace-cmd returned non-zero exit code {}'.format(process.returncode))
|
||||||
|
if error:
|
||||||
|
# logged at debug level, as trace-cmd always outputs some
|
||||||
|
# errors that seem benign.
|
||||||
|
self.logger.debug(error)
|
||||||
|
if os.path.isfile(destfile):
|
||||||
|
self.logger.debug('Verifying traces.')
|
||||||
|
with open(destfile) as fh:
|
||||||
|
for line in fh:
|
||||||
|
if 'EVENTS DROPPED' in line:
|
||||||
|
self.logger.warning('Dropped events detected.')
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.logger.debug('Trace verified.')
|
||||||
|
else:
|
||||||
|
self.logger.warning('Could not generate trace.txt.')
|
||||||
|
except OSError:
|
||||||
|
raise HostError('Could not find trace-cmd. Please make sure it is installed and is in PATH.')
|
||||||
|
|
||||||
|
def view(self, binfile):
|
||||||
|
check_output('{} {}'.format(self.kernelshark, binfile), shell=True)
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
|
self.target.remove(self.target.path.join(self.target.working_directory, OUTPUT_TRACE_FILE))
|
||||||
|
|
||||||
|
def mark_start(self):
|
||||||
|
self.target.write_value(self.marker_file, TRACE_MARKER_START, verify=False)
|
||||||
|
|
||||||
|
def mark_stop(self):
|
||||||
|
self.target.write_value(self.marker_file, TRACE_MARKER_STOP, verify=False)
|
||||||
|
|
||||||
|
def _set_buffer_size(self):
|
||||||
|
target_buffer_size = self.buffer_size
|
||||||
|
attempt_buffer_size = target_buffer_size
|
||||||
|
buffer_size = 0
|
||||||
|
floor = 1000 if target_buffer_size > 1000 else target_buffer_size
|
||||||
|
while attempt_buffer_size >= floor:
|
||||||
|
self.target.write_value(self.buffer_size_file, attempt_buffer_size, verify=False)
|
||||||
|
buffer_size = self.target.read_int(self.buffer_size_file)
|
||||||
|
if buffer_size == attempt_buffer_size:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
attempt_buffer_size -= self.buffer_size_step
|
||||||
|
if buffer_size == target_buffer_size:
|
||||||
|
return
|
||||||
|
while attempt_buffer_size < target_buffer_size:
|
||||||
|
attempt_buffer_size += self.buffer_size_step
|
||||||
|
self.target.write_value(self.buffer_size_file, attempt_buffer_size, verify=False)
|
||||||
|
buffer_size = self.target.read_int(self.buffer_size_file)
|
||||||
|
if attempt_buffer_size != buffer_size:
|
||||||
|
message = 'Failed to set trace buffer size to {}, value set was {}'
|
||||||
|
self.logger.warning(message.format(target_buffer_size, buffer_size))
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def _build_trace_events(events):
|
||||||
|
event_string = ' '.join(['-e {}'.format(e) for e in events])
|
||||||
|
return event_string
|
||||||
|
|
16
devlib/utils/__init__.py
Normal file
16
devlib/utils/__init__.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Copyright 2013-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
428
devlib/utils/android.py
Normal file
428
devlib/utils/android.py
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
# Copyright 2013-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Utility functions for working with Android devices through adb.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# pylint: disable=E1103
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from devlib.exception import TargetError, HostError
|
||||||
|
from devlib.utils.misc import check_output, which
|
||||||
|
from devlib.utils.misc import escape_single_quotes, escape_double_quotes
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('android')
|
||||||
|
|
||||||
|
MAX_ATTEMPTS = 5
|
||||||
|
AM_START_ERROR = re.compile(r"Error: Activity class {[\w|.|/]*} does not exist")
|
||||||
|
|
||||||
|
# See:
|
||||||
|
# http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels
|
||||||
|
ANDROID_VERSION_MAP = {
|
||||||
|
22: 'LOLLYPOP_MR1',
|
||||||
|
21: 'LOLLYPOP',
|
||||||
|
20: 'KITKAT_WATCH',
|
||||||
|
19: 'KITKAT',
|
||||||
|
18: 'JELLY_BEAN_MR2',
|
||||||
|
17: 'JELLY_BEAN_MR1',
|
||||||
|
16: 'JELLY_BEAN',
|
||||||
|
15: 'ICE_CREAM_SANDWICH_MR1',
|
||||||
|
14: 'ICE_CREAM_SANDWICH',
|
||||||
|
13: 'HONEYCOMB_MR2',
|
||||||
|
12: 'HONEYCOMB_MR1',
|
||||||
|
11: 'HONEYCOMB',
|
||||||
|
10: 'GINGERBREAD_MR1',
|
||||||
|
9: 'GINGERBREAD',
|
||||||
|
8: 'FROYO',
|
||||||
|
7: 'ECLAIR_MR1',
|
||||||
|
6: 'ECLAIR_0_1',
|
||||||
|
5: 'ECLAIR',
|
||||||
|
4: 'DONUT',
|
||||||
|
3: 'CUPCAKE',
|
||||||
|
2: 'BASE_1_1',
|
||||||
|
1: 'BASE',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Initialized in functions near the botton of the file
|
||||||
|
android_home = None
|
||||||
|
platform_tools = None
|
||||||
|
adb = None
|
||||||
|
aapt = None
|
||||||
|
fastboot = None
|
||||||
|
|
||||||
|
|
||||||
|
class AndroidProperties(object):
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
self._properties = {}
|
||||||
|
self.parse(text)
|
||||||
|
|
||||||
|
def parse(self, text):
|
||||||
|
self._properties = dict(re.findall(r'\[(.*?)\]:\s+\[(.*?)\]', text))
|
||||||
|
|
||||||
|
def iteritems(self):
|
||||||
|
return self._properties.iteritems()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._properties)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return self._properties.get(name)
|
||||||
|
|
||||||
|
__getitem__ = __getattr__
|
||||||
|
|
||||||
|
|
||||||
|
class AdbDevice(object):
|
||||||
|
|
||||||
|
def __init__(self, name, status):
|
||||||
|
self.name = name
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
def __cmp__(self, other):
|
||||||
|
if isinstance(other, AdbDevice):
|
||||||
|
return cmp(self.name, other.name)
|
||||||
|
else:
|
||||||
|
return cmp(self.name, other)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'AdbDevice({}, {})'.format(self.name, self.status)
|
||||||
|
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
|
||||||
|
class ApkInfo(object):
|
||||||
|
|
||||||
|
version_regex = re.compile(r"name='(?P<name>[^']+)' versionCode='(?P<vcode>[^']+)' versionName='(?P<vname>[^']+)'")
|
||||||
|
name_regex = re.compile(r"name='(?P<name>[^']+)'")
|
||||||
|
|
||||||
|
def __init__(self, path=None):
|
||||||
|
self.path = path
|
||||||
|
self.package = None
|
||||||
|
self.activity = None
|
||||||
|
self.label = None
|
||||||
|
self.version_name = None
|
||||||
|
self.version_code = None
|
||||||
|
self.parse(path)
|
||||||
|
|
||||||
|
def parse(self, apk_path):
|
||||||
|
_check_env()
|
||||||
|
command = [aapt, 'dump', 'badging', apk_path]
|
||||||
|
logger.debug(' '.join(command))
|
||||||
|
output = subprocess.check_output(command)
|
||||||
|
for line in output.split('\n'):
|
||||||
|
if line.startswith('application-label:'):
|
||||||
|
self.label = line.split(':')[1].strip().replace('\'', '')
|
||||||
|
elif line.startswith('package:'):
|
||||||
|
match = self.version_regex.search(line)
|
||||||
|
if match:
|
||||||
|
self.package = match.group('name')
|
||||||
|
self.version_code = match.group('vcode')
|
||||||
|
self.version_name = match.group('vname')
|
||||||
|
elif line.startswith('launchable-activity:'):
|
||||||
|
match = self.name_regex.search(line)
|
||||||
|
self.activity = match.group('name')
|
||||||
|
else:
|
||||||
|
pass # not interested
|
||||||
|
|
||||||
|
|
||||||
|
class AdbConnection(object):
|
||||||
|
|
||||||
|
# maintains the count of parallel active connections to a device, so that
|
||||||
|
# adb disconnect is not invoked untill all connections are closed
|
||||||
|
active_connections = defaultdict(int)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.device
|
||||||
|
|
||||||
|
def __init__(self, device=None, timeout=10):
|
||||||
|
self.timeout = timeout
|
||||||
|
if device is None:
|
||||||
|
device = adb_get_device(timeout=timeout)
|
||||||
|
self.device = device
|
||||||
|
adb_connect(self.device)
|
||||||
|
AdbConnection.active_connections[self.device] += 1
|
||||||
|
|
||||||
|
def push(self, source, dest, timeout=None):
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.timeout
|
||||||
|
command = 'push {} {}'.format(source, dest)
|
||||||
|
return adb_command(self.device, command, timeout=timeout)
|
||||||
|
|
||||||
|
def pull(self, source, dest, timeout=None):
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.timeout
|
||||||
|
command = 'pull {} {}'.format(source, dest)
|
||||||
|
return adb_command(self.device, command, timeout=timeout)
|
||||||
|
|
||||||
|
def execute(self, command, timeout=None, check_exit_code=False, as_root=False):
|
||||||
|
return adb_shell(self.device, command, timeout, check_exit_code, as_root)
|
||||||
|
|
||||||
|
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
|
||||||
|
return adb_background_shell(self.device, command, stdout, stderr, as_root)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
AdbConnection.active_connections[self.device] -= 1
|
||||||
|
if AdbConnection.active_connections[self.device] <= 0:
|
||||||
|
adb_disconnect(self.device)
|
||||||
|
del AdbConnection.active_connections[self.device]
|
||||||
|
|
||||||
|
def cancel_running_command(self):
|
||||||
|
# adbd multiplexes commands so that they don't interfer with each
|
||||||
|
# other, so there is no need to explicitly cancel a running command
|
||||||
|
# before the next one can be issued.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def fastboot_command(command, timeout=None):
|
||||||
|
_check_env()
|
||||||
|
full_command = "fastboot {}".format(command)
|
||||||
|
logger.debug(full_command)
|
||||||
|
output, _ = check_output(full_command, timeout, shell=True)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def fastboot_flash_partition(partition, path_to_image):
|
||||||
|
command = 'flash {} {}'.format(partition, path_to_image)
|
||||||
|
fastboot_command(command)
|
||||||
|
|
||||||
|
|
||||||
|
def adb_get_device(timeout=None):
|
||||||
|
"""
|
||||||
|
Returns the serial number of a connected android device.
|
||||||
|
|
||||||
|
If there are more than one device connected to the machine, or it could not
|
||||||
|
find any device connected, :class:`devlib.exceptions.HostError` is raised.
|
||||||
|
"""
|
||||||
|
# TODO this is a hacky way to issue a adb command to all listed devices
|
||||||
|
|
||||||
|
# The output of calling adb devices consists of a heading line then
|
||||||
|
# a list of the devices sperated by new line
|
||||||
|
# The last line is a blank new line. in otherwords, if there is a device found
|
||||||
|
# then the output length is 2 + (1 for each device)
|
||||||
|
start = time.time()
|
||||||
|
while True:
|
||||||
|
output = adb_command(None, "devices").splitlines() # pylint: disable=E1103
|
||||||
|
output_length = len(output)
|
||||||
|
if output_length == 3:
|
||||||
|
# output[1] is the 2nd line in the output which has the device name
|
||||||
|
# Splitting the line by '\t' gives a list of two indexes, which has
|
||||||
|
# device serial in 0 number and device type in 1.
|
||||||
|
return output[1].split('\t')[0]
|
||||||
|
elif output_length > 3:
|
||||||
|
message = '{} Android devices found; either explicitly specify ' +\
|
||||||
|
'the device you want, or make sure only one is connected.'
|
||||||
|
raise HostError(message.format(output_length - 2))
|
||||||
|
else:
|
||||||
|
if timeout < time.time() - start:
|
||||||
|
raise HostError('No device is connected and available')
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS):
|
||||||
|
_check_env()
|
||||||
|
tries = 0
|
||||||
|
output = None
|
||||||
|
while tries <= attempts:
|
||||||
|
tries += 1
|
||||||
|
if device:
|
||||||
|
command = 'adb connect {}'.format(device)
|
||||||
|
logger.debug(command)
|
||||||
|
output, _ = check_output(command, shell=True, timeout=timeout)
|
||||||
|
if _ping(device):
|
||||||
|
break
|
||||||
|
time.sleep(10)
|
||||||
|
else: # did not connect to the device
|
||||||
|
message = 'Could not connect to {}'.format(device or 'a device')
|
||||||
|
if output:
|
||||||
|
message += '; got: "{}"'.format(output)
|
||||||
|
raise HostError(message)
|
||||||
|
|
||||||
|
|
||||||
|
def adb_disconnect(device):
|
||||||
|
_check_env()
|
||||||
|
if not device:
|
||||||
|
return
|
||||||
|
if ":" in device:
|
||||||
|
command = "adb disconnect " + device
|
||||||
|
logger.debug(command)
|
||||||
|
retval = subprocess.call(command, stdout=open(os.devnull, 'wb'), shell=True)
|
||||||
|
if retval:
|
||||||
|
raise TargetError('"{}" returned {}'.format(command, retval))
|
||||||
|
|
||||||
|
|
||||||
|
def _ping(device):
|
||||||
|
_check_env()
|
||||||
|
device_string = ' -s {}'.format(device) if device else ''
|
||||||
|
command = "adb{} shell \"ls / > /dev/null\"".format(device_string)
|
||||||
|
logger.debug(command)
|
||||||
|
result = subprocess.call(command, stderr=subprocess.PIPE, shell=True)
|
||||||
|
if not result:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def adb_shell(device, command, timeout=None, check_exit_code=False, as_root=False): # NOQA
|
||||||
|
_check_env()
|
||||||
|
if as_root:
|
||||||
|
command = 'echo "{}" | su'.format(escape_double_quotes(command))
|
||||||
|
device_string = ' -s {}'.format(device) if device else ''
|
||||||
|
full_command = 'adb{} shell "{}"'.format(device_string,
|
||||||
|
escape_double_quotes(command))
|
||||||
|
logger.debug(full_command)
|
||||||
|
if check_exit_code:
|
||||||
|
actual_command = "adb{} shell '({}); echo $?'".format(device_string,
|
||||||
|
escape_single_quotes(command))
|
||||||
|
raw_output, error = check_output(actual_command, timeout, shell=True)
|
||||||
|
if raw_output:
|
||||||
|
try:
|
||||||
|
output, exit_code, _ = raw_output.rsplit('\n', 2)
|
||||||
|
except ValueError:
|
||||||
|
exit_code, _ = raw_output.rsplit('\n', 1)
|
||||||
|
output = ''
|
||||||
|
else: # raw_output is empty
|
||||||
|
exit_code = '969696' # just because
|
||||||
|
output = ''
|
||||||
|
|
||||||
|
exit_code = exit_code.strip()
|
||||||
|
if exit_code.isdigit():
|
||||||
|
if int(exit_code):
|
||||||
|
message = 'Got exit code {}\nfrom: {}\nSTDOUT: {}\nSTDERR: {}'
|
||||||
|
raise TargetError(message.format(exit_code, full_command, output, error))
|
||||||
|
elif AM_START_ERROR.findall(output):
|
||||||
|
message = 'Could not start activity; got the following:'
|
||||||
|
message += '\n{}'.format(AM_START_ERROR.findall(output)[0])
|
||||||
|
raise TargetError(message)
|
||||||
|
else: # not all digits
|
||||||
|
if AM_START_ERROR.findall(output):
|
||||||
|
message = 'Could not start activity; got the following:\n{}'
|
||||||
|
raise TargetError(message.format(AM_START_ERROR.findall(output)[0]))
|
||||||
|
else:
|
||||||
|
message = 'adb has returned early; did not get an exit code. '\
|
||||||
|
'Was kill-server invoked?'
|
||||||
|
raise TargetError(message)
|
||||||
|
else: # do not check exit code
|
||||||
|
output, _ = check_output(full_command, timeout, shell=True)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def adb_background_shell(device, command,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
as_root=False):
|
||||||
|
"""Runs the sepcified command in a subprocess, returning the the Popen object."""
|
||||||
|
_check_env()
|
||||||
|
if as_root:
|
||||||
|
command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
|
||||||
|
device_string = ' -s {}'.format(device) if device else ''
|
||||||
|
full_command = 'adb{} shell "{}"'.format(device_string, escape_double_quotes(command))
|
||||||
|
logger.debug(full_command)
|
||||||
|
return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True)
|
||||||
|
|
||||||
|
|
||||||
|
def adb_list_devices():
|
||||||
|
output = adb_command(None, 'devices')
|
||||||
|
devices = []
|
||||||
|
for line in output.splitlines():
|
||||||
|
parts = [p.strip() for p in line.split()]
|
||||||
|
if len(parts) == 2:
|
||||||
|
devices.append(AdbDevice(*parts))
|
||||||
|
return devices
|
||||||
|
|
||||||
|
|
||||||
|
def adb_command(device, command, timeout=None):
|
||||||
|
_check_env()
|
||||||
|
device_string = ' -s {}'.format(device) if device else ''
|
||||||
|
full_command = "adb{} {}".format(device_string, command)
|
||||||
|
logger.debug(full_command)
|
||||||
|
output, _ = check_output(full_command, timeout, shell=True)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
# Messy environment initialisation stuff...
|
||||||
|
|
||||||
|
class _AndroidEnvironment(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.android_home = None
|
||||||
|
self.platform_tools = None
|
||||||
|
self.adb = None
|
||||||
|
self.aapt = None
|
||||||
|
self.fastboot = None
|
||||||
|
|
||||||
|
|
||||||
|
def _initialize_with_android_home(env):
|
||||||
|
logger.debug('Using ANDROID_HOME from the environment.')
|
||||||
|
env.android_home = android_home
|
||||||
|
env.platform_tools = os.path.join(android_home, 'platform-tools')
|
||||||
|
os.environ['PATH'] += os.pathsep + env.platform_tools
|
||||||
|
_init_common(env)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _initialize_without_android_home(env):
|
||||||
|
if which('adb'):
|
||||||
|
env.adb = 'adb'
|
||||||
|
else:
|
||||||
|
raise HostError('ANDROID_HOME is not set and adb is not in PATH. '
|
||||||
|
'Have you installed Android SDK?')
|
||||||
|
logger.debug('Discovering ANDROID_HOME from adb path.')
|
||||||
|
env.platform_tools = os.path.dirname(env.adb)
|
||||||
|
env.android_home = os.path.dirname(env.platform_tools)
|
||||||
|
_init_common(env)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _init_common(env):
|
||||||
|
logger.debug('ANDROID_HOME: {}'.format(env.android_home))
|
||||||
|
build_tools_directory = os.path.join(env.android_home, 'build-tools')
|
||||||
|
if not os.path.isdir(build_tools_directory):
|
||||||
|
msg = '''ANDROID_HOME ({}) does not appear to have valid Android SDK install
|
||||||
|
(cannot find build-tools)'''
|
||||||
|
raise HostError(msg.format(env.android_home))
|
||||||
|
versions = os.listdir(build_tools_directory)
|
||||||
|
for version in reversed(sorted(versions)):
|
||||||
|
aapt_path = os.path.join(build_tools_directory, version, 'aapt')
|
||||||
|
if os.path.isfile(aapt_path):
|
||||||
|
logger.debug('Using aapt for version {}'.format(version))
|
||||||
|
env.aapt = aapt_path
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise HostError('aapt not found. Please make sure at least one Android '
|
||||||
|
'platform is installed.')
|
||||||
|
|
||||||
|
|
||||||
|
def _check_env():
|
||||||
|
global android_home, platform_tools, adb, aapt # pylint: disable=W0603
|
||||||
|
if not android_home:
|
||||||
|
android_home = os.getenv('ANDROID_HOME')
|
||||||
|
if android_home:
|
||||||
|
_env = _initialize_with_android_home(_AndroidEnvironment())
|
||||||
|
else:
|
||||||
|
_env = _initialize_without_android_home(_AndroidEnvironment())
|
||||||
|
android_home = _env.android_home
|
||||||
|
platform_tools = _env.platform_tools
|
||||||
|
adb = _env.adb
|
||||||
|
aapt = _env.aapt
|
552
devlib/utils/misc.py
Normal file
552
devlib/utils/misc.py
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
# Copyright 2013-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Miscellaneous functions that don't fit anywhere else.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import division
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
import threading
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import pkgutil
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
from operator import itemgetter
|
||||||
|
from itertools import groupby
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
|
||||||
|
# ABI --> architectures list
|
||||||
|
ABI_MAP = {
|
||||||
|
'armeabi': ['armeabi', 'armv7', 'armv7l', 'armv7el', 'armv7lh'],
|
||||||
|
'arm64': ['arm64', 'armv8', 'arm64-v8a', 'aarch64'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vendor ID --> CPU part ID --> CPU variant ID --> Core Name
|
||||||
|
# None means variant is not used.
|
||||||
|
CPU_PART_MAP = {
|
||||||
|
0x41: { # ARM
|
||||||
|
0x926: {None: 'ARM926'},
|
||||||
|
0x946: {None: 'ARM946'},
|
||||||
|
0x966: {None: 'ARM966'},
|
||||||
|
0xb02: {None: 'ARM11MPCore'},
|
||||||
|
0xb36: {None: 'ARM1136'},
|
||||||
|
0xb56: {None: 'ARM1156'},
|
||||||
|
0xb76: {None: 'ARM1176'},
|
||||||
|
0xc05: {None: 'A5'},
|
||||||
|
0xc07: {None: 'A7'},
|
||||||
|
0xc08: {None: 'A8'},
|
||||||
|
0xc09: {None: 'A9'},
|
||||||
|
0xc0f: {None: 'A15'},
|
||||||
|
0xc14: {None: 'R4'},
|
||||||
|
0xc15: {None: 'R5'},
|
||||||
|
0xc20: {None: 'M0'},
|
||||||
|
0xc21: {None: 'M1'},
|
||||||
|
0xc23: {None: 'M3'},
|
||||||
|
0xc24: {None: 'M4'},
|
||||||
|
0xc27: {None: 'M7'},
|
||||||
|
0xd03: {None: 'A53'},
|
||||||
|
0xd07: {None: 'A57'},
|
||||||
|
0xd08: {None: 'A72'},
|
||||||
|
},
|
||||||
|
0x4e: { # Nvidia
|
||||||
|
0x0: {None: 'Denver'},
|
||||||
|
},
|
||||||
|
0x51: { # Qualcomm
|
||||||
|
0x02d: {None: 'Scorpion'},
|
||||||
|
0x04d: {None: 'MSM8960'},
|
||||||
|
0x06f: { # Krait
|
||||||
|
0x2: 'Krait400',
|
||||||
|
0x3: 'Krait450',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
0x56: { # Marvell
|
||||||
|
0x131: {
|
||||||
|
0x2: 'Feroceon 88F6281',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_cpu_name(implementer, part, variant):
|
||||||
|
part_data = CPU_PART_MAP.get(implementer, {}).get(part, {})
|
||||||
|
if None in part_data: # variant does not determine core Name for this vendor
|
||||||
|
name = part_data[None]
|
||||||
|
else:
|
||||||
|
name = part_data.get(variant)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def preexec_function():
|
||||||
|
# Ignore the SIGINT signal by setting the handler to the standard
|
||||||
|
# signal handler SIG_IGN.
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
|
# Change process group in case we have to kill the subprocess and all of
|
||||||
|
# its children later.
|
||||||
|
# TODO: this is Unix-specific; would be good to find an OS-agnostic way
|
||||||
|
# to do this in case we wanna port WA to Windows.
|
||||||
|
os.setpgrp()
|
||||||
|
|
||||||
|
|
||||||
|
check_output_logger = logging.getLogger('check_output')
|
||||||
|
|
||||||
|
|
||||||
|
# Defined here rather than in devlib.exceptions due to module load dependencies
|
||||||
|
class TimeoutError(Exception):
|
||||||
|
"""Raised when a subprocess command times out. This is basically a ``WAError``-derived version
|
||||||
|
of ``subprocess.CalledProcessError``, the thinking being that while a timeout could be due to
|
||||||
|
programming error (e.g. not setting long enough timers), it is often due to some failure in the
|
||||||
|
environment, and there fore should be classed as a "user error"."""
|
||||||
|
|
||||||
|
def __init__(self, command, output):
|
||||||
|
super(TimeoutError, self).__init__('Timed out: {}'.format(command))
|
||||||
|
self.command = command
|
||||||
|
self.output = output
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '\n'.join([self.message, 'OUTPUT:', self.output or ''])
|
||||||
|
|
||||||
|
|
||||||
|
def check_output(command, timeout=None, ignore=None, inputtext=None, **kwargs):
|
||||||
|
"""This is a version of subprocess.check_output that adds a timeout parameter to kill
|
||||||
|
the subprocess if it does not return within the specified time."""
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
|
if ignore is None:
|
||||||
|
ignore = []
|
||||||
|
elif isinstance(ignore, int):
|
||||||
|
ignore = [ignore]
|
||||||
|
elif not isinstance(ignore, list) and ignore != 'all':
|
||||||
|
message = 'Invalid value for ignore parameter: "{}"; must be an int or a list'
|
||||||
|
raise ValueError(message.format(ignore))
|
||||||
|
if 'stdout' in kwargs:
|
||||||
|
raise ValueError('stdout argument not allowed, it will be overridden.')
|
||||||
|
|
||||||
|
def callback(pid):
|
||||||
|
try:
|
||||||
|
check_output_logger.debug('{} timed out; sending SIGKILL'.format(pid))
|
||||||
|
os.killpg(pid, signal.SIGKILL)
|
||||||
|
except OSError:
|
||||||
|
pass # process may have already terminated.
|
||||||
|
|
||||||
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
preexec_fn=preexec_function, **kwargs)
|
||||||
|
|
||||||
|
if timeout:
|
||||||
|
timer = threading.Timer(timeout, callback, [process.pid, ])
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
output, error = process.communicate(inputtext)
|
||||||
|
finally:
|
||||||
|
if timeout:
|
||||||
|
timer.cancel()
|
||||||
|
|
||||||
|
retcode = process.poll()
|
||||||
|
if retcode:
|
||||||
|
if retcode == -9: # killed, assume due to timeout callback
|
||||||
|
raise TimeoutError(command, output='\n'.join([output, error]))
|
||||||
|
elif ignore != 'all' and retcode not in ignore:
|
||||||
|
raise subprocess.CalledProcessError(retcode, command, output='\n'.join([output, error]))
|
||||||
|
return output, error
|
||||||
|
|
||||||
|
|
||||||
|
def walk_modules(path):
|
||||||
|
"""
|
||||||
|
Given package name, return a list of all modules (including submodules, etc)
|
||||||
|
in that package.
|
||||||
|
|
||||||
|
"""
|
||||||
|
root_mod = __import__(path, {}, {}, [''])
|
||||||
|
mods = [root_mod]
|
||||||
|
for _, name, ispkg in pkgutil.iter_modules(root_mod.__path__):
|
||||||
|
submod_path = '.'.join([path, name])
|
||||||
|
if ispkg:
|
||||||
|
mods.extend(walk_modules(submod_path))
|
||||||
|
else:
|
||||||
|
submod = __import__(submod_path, {}, {}, [''])
|
||||||
|
mods.append(submod)
|
||||||
|
return mods
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_directory_exists(dirpath):
|
||||||
|
"""A filter for directory paths to ensure they exist."""
|
||||||
|
if not os.path.isdir(dirpath):
|
||||||
|
os.makedirs(dirpath)
|
||||||
|
return dirpath
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_file_directory_exists(filepath):
|
||||||
|
"""
|
||||||
|
A filter for file paths to ensure the directory of the
|
||||||
|
file exists and the file can be created there. The file
|
||||||
|
itself is *not* going to be created if it doesn't already
|
||||||
|
exist.
|
||||||
|
|
||||||
|
"""
|
||||||
|
ensure_directory_exists(os.path.dirname(filepath))
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
|
||||||
|
def merge_dicts(*args, **kwargs):
|
||||||
|
if not len(args) >= 2:
|
||||||
|
raise ValueError('Must specify at least two dicts to merge.')
|
||||||
|
func = partial(_merge_two_dicts, **kwargs)
|
||||||
|
return reduce(func, args)
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_two_dicts(base, other, list_duplicates='all', match_types=False, # pylint: disable=R0912,R0914
|
||||||
|
dict_type=dict, should_normalize=True, should_merge_lists=True):
|
||||||
|
"""Merge dicts normalizing their keys."""
|
||||||
|
merged = dict_type()
|
||||||
|
base_keys = base.keys()
|
||||||
|
other_keys = other.keys()
|
||||||
|
norm = normalize if should_normalize else lambda x, y: x
|
||||||
|
|
||||||
|
base_only = []
|
||||||
|
other_only = []
|
||||||
|
both = []
|
||||||
|
union = []
|
||||||
|
for k in base_keys:
|
||||||
|
if k in other_keys:
|
||||||
|
both.append(k)
|
||||||
|
else:
|
||||||
|
base_only.append(k)
|
||||||
|
union.append(k)
|
||||||
|
for k in other_keys:
|
||||||
|
if k in base_keys:
|
||||||
|
union.append(k)
|
||||||
|
else:
|
||||||
|
union.append(k)
|
||||||
|
other_only.append(k)
|
||||||
|
|
||||||
|
for k in union:
|
||||||
|
if k in base_only:
|
||||||
|
merged[k] = norm(base[k], dict_type)
|
||||||
|
elif k in other_only:
|
||||||
|
merged[k] = norm(other[k], dict_type)
|
||||||
|
elif k in both:
|
||||||
|
base_value = base[k]
|
||||||
|
other_value = other[k]
|
||||||
|
base_type = type(base_value)
|
||||||
|
other_type = type(other_value)
|
||||||
|
if (match_types and (base_type != other_type) and
|
||||||
|
(base_value is not None) and (other_value is not None)):
|
||||||
|
raise ValueError('Type mismatch for {} got {} ({}) and {} ({})'.format(k, base_value, base_type,
|
||||||
|
other_value, other_type))
|
||||||
|
if isinstance(base_value, dict):
|
||||||
|
merged[k] = _merge_two_dicts(base_value, other_value, list_duplicates, match_types, dict_type)
|
||||||
|
elif isinstance(base_value, list):
|
||||||
|
if should_merge_lists:
|
||||||
|
merged[k] = _merge_two_lists(base_value, other_value, list_duplicates, dict_type)
|
||||||
|
else:
|
||||||
|
merged[k] = _merge_two_lists([], other_value, list_duplicates, dict_type)
|
||||||
|
|
||||||
|
elif isinstance(base_value, set):
|
||||||
|
merged[k] = norm(base_value.union(other_value), dict_type)
|
||||||
|
else:
|
||||||
|
merged[k] = norm(other_value, dict_type)
|
||||||
|
else: # Should never get here
|
||||||
|
raise AssertionError('Unexpected merge key: {}'.format(k))
|
||||||
|
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def merge_lists(*args, **kwargs):
|
||||||
|
if not len(args) >= 2:
|
||||||
|
raise ValueError('Must specify at least two lists to merge.')
|
||||||
|
func = partial(_merge_two_lists, **kwargs)
|
||||||
|
return reduce(func, args)
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_two_lists(base, other, duplicates='all', dict_type=dict): # pylint: disable=R0912
|
||||||
|
"""
|
||||||
|
Merge lists, normalizing their entries.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
|
||||||
|
:base, other: the two lists to be merged. ``other`` will be merged on
|
||||||
|
top of base.
|
||||||
|
:duplicates: Indicates the strategy of handling entries that appear
|
||||||
|
in both lists. ``all`` will keep occurrences from both
|
||||||
|
lists; ``first`` will only keep occurrences from
|
||||||
|
``base``; ``last`` will only keep occurrences from
|
||||||
|
``other``;
|
||||||
|
|
||||||
|
.. note:: duplicate entries that appear in the *same* list
|
||||||
|
will never be removed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not isiterable(base):
|
||||||
|
base = [base]
|
||||||
|
if not isiterable(other):
|
||||||
|
other = [other]
|
||||||
|
if duplicates == 'all':
|
||||||
|
merged_list = []
|
||||||
|
for v in normalize(base, dict_type) + normalize(other, dict_type):
|
||||||
|
if not _check_remove_item(merged_list, v):
|
||||||
|
merged_list.append(v)
|
||||||
|
return merged_list
|
||||||
|
elif duplicates == 'first':
|
||||||
|
base_norm = normalize(base, dict_type)
|
||||||
|
merged_list = normalize(base, dict_type)
|
||||||
|
for v in base_norm:
|
||||||
|
_check_remove_item(merged_list, v)
|
||||||
|
for v in normalize(other, dict_type):
|
||||||
|
if not _check_remove_item(merged_list, v):
|
||||||
|
if v not in base_norm:
|
||||||
|
merged_list.append(v) # pylint: disable=no-member
|
||||||
|
return merged_list
|
||||||
|
elif duplicates == 'last':
|
||||||
|
other_norm = normalize(other, dict_type)
|
||||||
|
merged_list = []
|
||||||
|
for v in normalize(base, dict_type):
|
||||||
|
if not _check_remove_item(merged_list, v):
|
||||||
|
if v not in other_norm:
|
||||||
|
merged_list.append(v)
|
||||||
|
for v in other_norm:
|
||||||
|
if not _check_remove_item(merged_list, v):
|
||||||
|
merged_list.append(v)
|
||||||
|
return merged_list
|
||||||
|
else:
|
||||||
|
raise ValueError('Unexpected value for list duplicates argument: {}. '.format(duplicates) +
|
||||||
|
'Must be in {"all", "first", "last"}.')
|
||||||
|
|
||||||
|
|
||||||
|
def _check_remove_item(the_list, item):
|
||||||
|
"""Helper function for merge_lists that implements checking wether an items
|
||||||
|
should be removed from the list and doing so if needed. Returns ``True`` if
|
||||||
|
the item has been removed and ``False`` otherwise."""
|
||||||
|
if not isinstance(item, basestring):
|
||||||
|
return False
|
||||||
|
if not item.startswith('~'):
|
||||||
|
return False
|
||||||
|
actual_item = item[1:]
|
||||||
|
if actual_item in the_list:
|
||||||
|
del the_list[the_list.index(actual_item)]
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(value, dict_type=dict):
|
||||||
|
"""Normalize values. Recursively normalizes dict keys to be lower case,
|
||||||
|
no surrounding whitespace, underscore-delimited strings."""
|
||||||
|
if isinstance(value, dict):
|
||||||
|
normalized = dict_type()
|
||||||
|
for k, v in value.iteritems():
|
||||||
|
key = k.strip().lower().replace(' ', '_')
|
||||||
|
normalized[key] = normalize(v, dict_type)
|
||||||
|
return normalized
|
||||||
|
elif isinstance(value, list):
|
||||||
|
return [normalize(v, dict_type) for v in value]
|
||||||
|
elif isinstance(value, tuple):
|
||||||
|
return tuple([normalize(v, dict_type) for v in value])
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def convert_new_lines(text):
|
||||||
|
""" Convert new lines to a common format. """
|
||||||
|
return text.replace('\r\n', '\n').replace('\r', '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def escape_quotes(text):
|
||||||
|
"""Escape quotes, and escaped quotes, in the specified text."""
|
||||||
|
return re.sub(r'\\("|\')', r'\\\\\1', text).replace('\'', '\\\'').replace('\"', '\\\"')
|
||||||
|
|
||||||
|
|
||||||
|
def escape_single_quotes(text):
|
||||||
|
"""Escape single quotes, and escaped single quotes, in the specified text."""
|
||||||
|
return re.sub(r'\\("|\')', r'\\\\\1', text).replace('\'', '\'\\\'\'')
|
||||||
|
|
||||||
|
|
||||||
|
def escape_double_quotes(text):
|
||||||
|
"""Escape double quotes, and escaped double quotes, in the specified text."""
|
||||||
|
return re.sub(r'\\("|\')', r'\\\\\1', text).replace('\"', '\\\"')
|
||||||
|
|
||||||
|
|
||||||
|
def getch(count=1):
|
||||||
|
"""Read ``count`` characters from standard input."""
|
||||||
|
if os.name == 'nt':
|
||||||
|
import msvcrt # pylint: disable=F0401
|
||||||
|
return ''.join([msvcrt.getch() for _ in xrange(count)])
|
||||||
|
else: # assume Unix
|
||||||
|
import tty # NOQA
|
||||||
|
import termios # NOQA
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setraw(sys.stdin.fileno())
|
||||||
|
ch = sys.stdin.read(count)
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
return ch
|
||||||
|
|
||||||
|
|
||||||
|
def isiterable(obj):
|
||||||
|
"""Returns ``True`` if the specified object is iterable and
|
||||||
|
*is not a string type*, ``False`` otherwise."""
|
||||||
|
return hasattr(obj, '__iter__') and not isinstance(obj, basestring)
|
||||||
|
|
||||||
|
|
||||||
|
def as_relative(path):
|
||||||
|
"""Convert path to relative by stripping away the leading '/' on UNIX or
|
||||||
|
the equivant on other platforms."""
|
||||||
|
path = os.path.splitdrive(path)[1]
|
||||||
|
return path.lstrip(os.sep)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cpu_mask(cores):
|
||||||
|
"""Return a string with the hex for the cpu mask for the specified core numbers."""
|
||||||
|
mask = 0
|
||||||
|
for i in cores:
|
||||||
|
mask |= 1 << i
|
||||||
|
return '0x{0:x}'.format(mask)
|
||||||
|
|
||||||
|
|
||||||
|
def which(name):
|
||||||
|
"""Platform-independent version of UNIX which utility."""
|
||||||
|
if os.name == 'nt':
|
||||||
|
paths = os.getenv('PATH').split(os.pathsep)
|
||||||
|
exts = os.getenv('PATHEXT').split(os.pathsep)
|
||||||
|
for path in paths:
|
||||||
|
testpath = os.path.join(path, name)
|
||||||
|
if os.path.isfile(testpath):
|
||||||
|
return testpath
|
||||||
|
for ext in exts:
|
||||||
|
testpathext = testpath + ext
|
||||||
|
if os.path.isfile(testpathext):
|
||||||
|
return testpathext
|
||||||
|
return None
|
||||||
|
else: # assume UNIX-like
|
||||||
|
try:
|
||||||
|
return check_output(['which', name])[0].strip() # pylint: disable=E1103
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_bash_color_regex = re.compile('\x1b\\[[0-9;]+m')
|
||||||
|
|
||||||
|
|
||||||
|
def strip_bash_colors(text):
|
||||||
|
return _bash_color_regex.sub('', text)
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_string(length):
|
||||||
|
"""Returns a random ASCII string of the specified length)."""
|
||||||
|
return ''.join(random.choice(string.ascii_letters + string.digits) for _ in xrange(length))
|
||||||
|
|
||||||
|
|
||||||
|
class LoadSyntaxError(Exception):
|
||||||
|
|
||||||
|
def __init__(self, message, filepath, lineno):
|
||||||
|
super(LoadSyntaxError, self).__init__(message)
|
||||||
|
self.filepath = filepath
|
||||||
|
self.lineno = lineno
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
message = 'Syntax Error in {}, line {}:\n\t{}'
|
||||||
|
return message.format(self.filepath, self.lineno, self.message)
|
||||||
|
|
||||||
|
|
||||||
|
RAND_MOD_NAME_LEN = 30
|
||||||
|
BAD_CHARS = string.punctuation + string.whitespace
|
||||||
|
TRANS_TABLE = string.maketrans(BAD_CHARS, '_' * len(BAD_CHARS))
|
||||||
|
|
||||||
|
|
||||||
|
def to_identifier(text):
|
||||||
|
"""Converts text to a valid Python identifier by replacing all
|
||||||
|
whitespace and punctuation."""
|
||||||
|
return re.sub('_+', '_', text.translate(TRANS_TABLE))
|
||||||
|
|
||||||
|
|
||||||
|
def unique(alist):
|
||||||
|
"""
|
||||||
|
Returns a list containing only unique elements from the input list (but preserves
|
||||||
|
order, unlike sets).
|
||||||
|
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for item in alist:
|
||||||
|
if item not in result:
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def ranges_to_list(ranges_string):
|
||||||
|
"""Converts a sysfs-style ranges string, e.g. ``"0,2-4"``, into a list ,e.g ``[0,2,3,4]``"""
|
||||||
|
values = []
|
||||||
|
for rg in ranges_string.split(','):
|
||||||
|
if '-' in rg:
|
||||||
|
first, last = map(int, rg.split('-'))
|
||||||
|
values.extend(xrange(first, last + 1))
|
||||||
|
else:
|
||||||
|
values.append(int(rg))
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def list_to_ranges(values):
|
||||||
|
"""Converts a list, e.g ``[0,2,3,4]``, into a sysfs-style ranges string, e.g. ``"0,2-4"``"""
|
||||||
|
range_groups = []
|
||||||
|
for _, g in groupby(enumerate(values), lambda (i, x): i - x):
|
||||||
|
range_groups.append(map(itemgetter(1), g))
|
||||||
|
range_strings = []
|
||||||
|
for group in range_groups:
|
||||||
|
if len(group) == 1:
|
||||||
|
range_strings.append(str(group[0]))
|
||||||
|
else:
|
||||||
|
range_strings.append('{}-{}'.format(group[0], group[-1]))
|
||||||
|
return ','.join(range_strings)
|
||||||
|
|
||||||
|
|
||||||
|
def list_to_mask(values, base=0x0):
|
||||||
|
"""Converts the specified list of integer values into
|
||||||
|
a bit mask for those values. Optinally, the list can be
|
||||||
|
applied to an existing mask."""
|
||||||
|
for v in values:
|
||||||
|
base |= (1 << v)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def mask_to_list(mask):
|
||||||
|
"""Converts the specfied integer bitmask into a list of
|
||||||
|
indexes of bits that are set in the mask."""
|
||||||
|
size = len(bin(mask)) - 2 # because of "0b"
|
||||||
|
return [size - i - 1 for i in xrange(size)
|
||||||
|
if mask & (1 << size - i - 1)]
|
||||||
|
|
||||||
|
|
||||||
|
__memo_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
def memoized(func):
|
||||||
|
"""A decorator for memoizing functions and methods."""
|
||||||
|
func_id = repr(func)
|
||||||
|
|
||||||
|
def memoize_wrapper(*args, **kwargs):
|
||||||
|
id_string = func_id + ','.join([str(id(a)) for a in args])
|
||||||
|
id_string += ','.join('{}={}'.format(k, v)
|
||||||
|
for k, v in kwargs.iteritems())
|
||||||
|
if id_string not in __memo_cache:
|
||||||
|
__memo_cache[id_string] = func(*args, **kwargs)
|
||||||
|
return __memo_cache[id_string]
|
||||||
|
|
||||||
|
return memoize_wrapper
|
||||||
|
|
107
devlib/utils/serial_port.py
Normal file
107
devlib/utils/serial_port.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# Copyright 2013-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from logging import Logger
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import fdpexpect
|
||||||
|
# Adding pexpect exceptions into this module's namespace
|
||||||
|
from pexpect import EOF, TIMEOUT # NOQA pylint: disable=W0611
|
||||||
|
|
||||||
|
from devlib.exception import HostError
|
||||||
|
|
||||||
|
|
||||||
|
def pulse_dtr(conn, state=True, duration=0.1):
|
||||||
|
"""Set the DTR line of the specified serial connection to the specified state
|
||||||
|
for the specified duration (note: the initial state of the line is *not* checked."""
|
||||||
|
conn.setDTR(state)
|
||||||
|
time.sleep(duration)
|
||||||
|
conn.setDTR(not state)
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection(timeout, init_dtr=None, logcls=Logger,
|
||||||
|
*args, **kwargs):
|
||||||
|
if init_dtr is not None:
|
||||||
|
kwargs['dsrdtr'] = True
|
||||||
|
try:
|
||||||
|
conn = serial.Serial(*args, **kwargs)
|
||||||
|
except serial.SerialException as e:
|
||||||
|
raise HostError(e.message)
|
||||||
|
if init_dtr is not None:
|
||||||
|
conn.setDTR(init_dtr)
|
||||||
|
conn.nonblocking()
|
||||||
|
conn.flushOutput()
|
||||||
|
target = fdpexpect.fdspawn(conn.fileno(), timeout=timeout)
|
||||||
|
target.logfile_read = logcls('read')
|
||||||
|
target.logfile_send = logcls('send')
|
||||||
|
|
||||||
|
# Monkey-patching sendline to introduce a short delay after
|
||||||
|
# chacters are sent to the serial. If two sendline s are issued
|
||||||
|
# one after another the second one might start putting characters
|
||||||
|
# into the serial device before the first one has finished, causing
|
||||||
|
# corruption. The delay prevents that.
|
||||||
|
tsln = target.sendline
|
||||||
|
|
||||||
|
def sendline(x):
|
||||||
|
tsln(x)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
target.sendline = sendline
|
||||||
|
return target, conn
|
||||||
|
|
||||||
|
|
||||||
|
def write_characters(conn, line, delay=0.05):
|
||||||
|
"""Write a single line out to serial charcter-by-character. This will ensure that nothing will
|
||||||
|
be dropped for longer lines."""
|
||||||
|
line = line.rstrip('\r\n')
|
||||||
|
for c in line:
|
||||||
|
conn.send(c)
|
||||||
|
time.sleep(delay)
|
||||||
|
conn.sendline('')
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def open_serial_connection(timeout, get_conn=False, init_dtr=None,
|
||||||
|
logcls=Logger, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Opens a serial connection to a device.
|
||||||
|
|
||||||
|
:param timeout: timeout for the fdpexpect spawn object.
|
||||||
|
:param conn: ``bool`` that specfies whether the underlying connection
|
||||||
|
object should be yielded as well.
|
||||||
|
:param init_dtr: specifies the initial DTR state stat should be set.
|
||||||
|
|
||||||
|
All arguments are passed into the __init__ of serial.Serial. See
|
||||||
|
pyserial documentation for details:
|
||||||
|
|
||||||
|
http://pyserial.sourceforge.net/pyserial_api.html#serial.Serial
|
||||||
|
|
||||||
|
:returns: a pexpect spawn object connected to the device.
|
||||||
|
See: http://pexpect.sourceforge.net/pexpect.html
|
||||||
|
|
||||||
|
"""
|
||||||
|
target, conn = get_connection(timeout, init_dtr=init_dtr,
|
||||||
|
logcls=logcls, *args, **kwargs)
|
||||||
|
if get_conn:
|
||||||
|
yield target, conn
|
||||||
|
else:
|
||||||
|
yield target
|
||||||
|
|
||||||
|
target.close() # Closes the file descriptor used by the conn.
|
||||||
|
del conn
|
||||||
|
|
261
devlib/utils/ssh.py
Normal file
261
devlib/utils/ssh.py
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
# Copyright 2014-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import pxssh
|
||||||
|
from pexpect import EOF, TIMEOUT, spawn
|
||||||
|
|
||||||
|
from devlib.exception import HostError, TargetError, TimeoutError
|
||||||
|
from devlib.utils.misc import which, strip_bash_colors, escape_single_quotes, check_output
|
||||||
|
|
||||||
|
|
||||||
|
ssh = None
|
||||||
|
scp = None
|
||||||
|
sshpass = None
|
||||||
|
|
||||||
|
logger = logging.getLogger('ssh')
|
||||||
|
|
||||||
|
|
||||||
|
def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeout=10, telnet=False):
|
||||||
|
_check_env()
|
||||||
|
if telnet:
|
||||||
|
if keyfile:
|
||||||
|
raise ValueError('keyfile may not be used with a telnet connection.')
|
||||||
|
conn = TelnetConnection()
|
||||||
|
else: # ssh
|
||||||
|
conn = pxssh.pxssh()
|
||||||
|
try:
|
||||||
|
if keyfile:
|
||||||
|
conn.login(host, username, ssh_key=keyfile, port=port, login_timeout=timeout)
|
||||||
|
else:
|
||||||
|
conn.login(host, username, password, port=port, login_timeout=timeout)
|
||||||
|
except EOF:
|
||||||
|
raise TargetError('Could not connect to {}; is the host name correct?'.format(host))
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
class TelnetConnection(pxssh.pxssh):
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
|
|
||||||
|
def login(self, server, username, password='', original_prompt=r'[#$]', login_timeout=10,
|
||||||
|
auto_prompt_reset=True, sync_multiplier=1):
|
||||||
|
cmd = 'telnet -l {} {}'.format(username, server)
|
||||||
|
|
||||||
|
spawn._spawn(self, cmd) # pylint: disable=protected-access
|
||||||
|
i = self.expect('(?i)(?:password)', timeout=login_timeout)
|
||||||
|
if i == 0:
|
||||||
|
self.sendline(password)
|
||||||
|
i = self.expect([original_prompt, 'Login incorrect'], timeout=login_timeout)
|
||||||
|
else:
|
||||||
|
raise pxssh.ExceptionPxssh('could not log in: did not see a password prompt')
|
||||||
|
|
||||||
|
if i:
|
||||||
|
raise pxssh.ExceptionPxssh('could not log in: password was incorrect')
|
||||||
|
|
||||||
|
if not self.sync_original_prompt(sync_multiplier):
|
||||||
|
self.close()
|
||||||
|
raise pxssh.ExceptionPxssh('could not synchronize with original prompt')
|
||||||
|
|
||||||
|
if auto_prompt_reset:
|
||||||
|
if not self.set_unique_prompt():
|
||||||
|
self.close()
|
||||||
|
message = 'could not set shell prompt (recieved: {}, expected: {}).'
|
||||||
|
raise pxssh.ExceptionPxssh(message.format(self.before, self.PROMPT))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_keyfile(keyfile):
|
||||||
|
"""
|
||||||
|
keyfile must have the right access premissions in order to be useable. If the specified
|
||||||
|
file doesn't, create a temporary copy and set the right permissions for that.
|
||||||
|
|
||||||
|
Returns either the ``keyfile`` (if the permissions on it are correct) or the path to a
|
||||||
|
temporary copy with the right permissions.
|
||||||
|
"""
|
||||||
|
desired_mask = stat.S_IWUSR | stat.S_IRUSR
|
||||||
|
actual_mask = os.stat(keyfile).st_mode & 0xFF
|
||||||
|
if actual_mask != desired_mask:
|
||||||
|
tmp_file = os.path.join(tempfile.gettempdir(), os.path.basename(keyfile))
|
||||||
|
shutil.copy(keyfile, tmp_file)
|
||||||
|
os.chmod(tmp_file, desired_mask)
|
||||||
|
return tmp_file
|
||||||
|
else: # permissions on keyfile are OK
|
||||||
|
return keyfile
|
||||||
|
|
||||||
|
|
||||||
|
class SshConnection(object):
|
||||||
|
|
||||||
|
default_password_prompt = '[sudo] password'
|
||||||
|
max_cancel_attempts = 5
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.host
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
host,
|
||||||
|
username,
|
||||||
|
password=None,
|
||||||
|
keyfile=None,
|
||||||
|
port=None,
|
||||||
|
timeout=10,
|
||||||
|
telnet=False,
|
||||||
|
password_prompt=None,
|
||||||
|
):
|
||||||
|
self.host = host
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.keyfile = check_keyfile(keyfile) if keyfile else keyfile
|
||||||
|
self.port = port
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
|
||||||
|
logger.debug('Logging in {}@{}'.format(username, host))
|
||||||
|
self.conn = ssh_get_shell(host, username, password, self.keyfile, port, timeout, telnet)
|
||||||
|
|
||||||
|
def push(self, source, dest, timeout=30):
|
||||||
|
dest = '{}@{}:{}'.format(self.username, self.host, dest)
|
||||||
|
return self._scp(source, dest, timeout)
|
||||||
|
|
||||||
|
def pull(self, source, dest, timeout=30):
|
||||||
|
source = '{}@{}:{}'.format(self.username, self.host, source)
|
||||||
|
return self._scp(source, dest, timeout)
|
||||||
|
|
||||||
|
def execute(self, command, timeout=None, check_exit_code=True, as_root=False, strip_colors=True):
|
||||||
|
with self.lock:
|
||||||
|
output = self._execute_and_wait_for_prompt(command, timeout, as_root, strip_colors)
|
||||||
|
if check_exit_code:
|
||||||
|
exit_code_text = self._execute_and_wait_for_prompt('echo $?', strip_colors=strip_colors, log=False)
|
||||||
|
try:
|
||||||
|
exit_code = int(exit_code_text.split()[0])
|
||||||
|
if exit_code:
|
||||||
|
message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
|
||||||
|
raise TargetError(message.format(exit_code, command, output))
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text))
|
||||||
|
return output
|
||||||
|
|
||||||
|
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE):
|
||||||
|
port_string = '-p {}'.format(self.port) if self.port else ''
|
||||||
|
keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
|
||||||
|
command = '{} {} {} {}@{} {}'.format(ssh, keyfile_string, port_string, self.username, self.host, command)
|
||||||
|
logger.debug(command)
|
||||||
|
if self.password:
|
||||||
|
command = _give_password(self.password, command)
|
||||||
|
return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
logger.debug('Logging out {}@{}'.format(self.username, self.host))
|
||||||
|
self.conn.logout()
|
||||||
|
|
||||||
|
def cancel_running_command(self):
|
||||||
|
# simulate impatiently hitting ^C until command prompt appears
|
||||||
|
logger.debug('Sending ^C')
|
||||||
|
for _ in xrange(self.max_cancel_attempts):
|
||||||
|
self.conn.sendline(chr(3))
|
||||||
|
if self.conn.prompt(0.1):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _execute_and_wait_for_prompt(self, command, timeout=None, as_root=False, strip_colors=True, log=True):
|
||||||
|
self.conn.prompt(0.1) # clear an existing prompt if there is one.
|
||||||
|
if as_root:
|
||||||
|
command = "sudo -- sh -c '{}'".format(escape_single_quotes(command))
|
||||||
|
if log:
|
||||||
|
logger.debug(command)
|
||||||
|
self.conn.sendline(command)
|
||||||
|
if self.password:
|
||||||
|
index = self.conn.expect_exact([self.password_prompt, TIMEOUT], timeout=0.5)
|
||||||
|
if index == 0:
|
||||||
|
self.conn.sendline(self.password)
|
||||||
|
else: # not as_root
|
||||||
|
if log:
|
||||||
|
logger.debug(command)
|
||||||
|
self.conn.sendline(command)
|
||||||
|
timed_out = self._wait_for_prompt(timeout)
|
||||||
|
# the regex removes line breaks potential introduced when writing
|
||||||
|
# command to shell.
|
||||||
|
output = process_backspaces(self.conn.before)
|
||||||
|
output = re.sub(r'\r([^\n])', r'\1', output)
|
||||||
|
if '\r\n' in output: # strip the echoed command
|
||||||
|
output = output.split('\r\n', 1)[1]
|
||||||
|
if timed_out:
|
||||||
|
self.cancel_running_command()
|
||||||
|
raise TimeoutError(command, output)
|
||||||
|
if strip_colors:
|
||||||
|
output = strip_bash_colors(output)
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _wait_for_prompt(self, timeout=None):
|
||||||
|
if timeout:
|
||||||
|
return not self.conn.prompt(timeout)
|
||||||
|
else: # cannot timeout; wait forever
|
||||||
|
while not self.conn.prompt(1):
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _scp(self, source, dest, timeout=30):
|
||||||
|
# NOTE: the version of scp in Ubuntu 12.04 occasionally (and bizarrely)
|
||||||
|
# fails to connect to a device if port is explicitly specified using -P
|
||||||
|
# option, even if it is the default port, 22. To minimize this problem,
|
||||||
|
# only specify -P for scp if the port is *not* the default.
|
||||||
|
port_string = '-P {}'.format(self.port) if (self.port and self.port != 22) else ''
|
||||||
|
keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
|
||||||
|
command = '{} -r {} {} {} {}'.format(scp, keyfile_string, port_string, source, dest)
|
||||||
|
pass_string = ''
|
||||||
|
logger.debug(command)
|
||||||
|
if self.password:
|
||||||
|
command = _give_password(self.password, command)
|
||||||
|
try:
|
||||||
|
check_output(command, timeout=timeout, shell=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
raise subprocess.CalledProcessError(e.returncode, e.cmd.replace(pass_string, ''), e.output)
|
||||||
|
except TimeoutError as e:
|
||||||
|
raise TimeoutError(e.command.replace(pass_string, ''), e.output)
|
||||||
|
|
||||||
|
|
||||||
|
def _give_password(password, command):
|
||||||
|
if not sshpass:
|
||||||
|
raise HostError('Must have sshpass installed on the host in order to use password-based auth.')
|
||||||
|
pass_string = "sshpass -p '{}' ".format(password)
|
||||||
|
return pass_string + command
|
||||||
|
|
||||||
|
|
||||||
|
def _check_env():
|
||||||
|
global ssh, scp, sshpass # pylint: disable=global-statement
|
||||||
|
if not ssh:
|
||||||
|
ssh = which('ssh')
|
||||||
|
scp = which('scp')
|
||||||
|
sshpass = which('sshpass')
|
||||||
|
if not (ssh and scp):
|
||||||
|
raise HostError('OpenSSH must be installed on the host.')
|
||||||
|
|
||||||
|
|
||||||
|
def process_backspaces(text):
|
||||||
|
chars = []
|
||||||
|
for c in text:
|
||||||
|
if c == chr(8) and chars: # backspace
|
||||||
|
chars.pop()
|
||||||
|
else:
|
||||||
|
chars.append(c)
|
||||||
|
return ''.join(chars)
|
113
devlib/utils/types.py
Normal file
113
devlib/utils/types.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# Copyright 2014-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Routines for doing various type conversions. These usually embody some higher-level
|
||||||
|
semantics than are present in standard Python types (e.g. ``boolean`` will convert the
|
||||||
|
string ``"false"`` to ``False``, where as non-empty strings are usually considered to be
|
||||||
|
``True``).
|
||||||
|
|
||||||
|
A lot of these are intened to stpecify type conversions declaratively in place like
|
||||||
|
``Parameter``'s ``kind`` argument. These are basically "hacks" around the fact that Python
|
||||||
|
is not the best language to use for configuration.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
|
||||||
|
from devlib.utils.misc import isiterable, to_identifier, ranges_to_list, list_to_mask
|
||||||
|
|
||||||
|
|
||||||
|
def identifier(text):
|
||||||
|
"""Converts text to a valid Python identifier by replacing all
|
||||||
|
whitespace and punctuation."""
|
||||||
|
return to_identifier(text)
|
||||||
|
|
||||||
|
|
||||||
|
def boolean(value):
|
||||||
|
"""
|
||||||
|
Returns bool represented by the value. This is different from
|
||||||
|
calling the builtin bool() in that it will interpret string representations.
|
||||||
|
e.g. boolean('0') and boolean('false') will both yield False.
|
||||||
|
|
||||||
|
"""
|
||||||
|
false_strings = ['', '0', 'n', 'no', 'off']
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
value = value.lower()
|
||||||
|
if value in false_strings or 'false'.startswith(value):
|
||||||
|
return False
|
||||||
|
return bool(value)
|
||||||
|
|
||||||
|
|
||||||
|
def integer(value):
|
||||||
|
"""Handles conversions for string respresentations of binary, octal and hex."""
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
return int(value, 0)
|
||||||
|
else:
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
|
||||||
|
def numeric(value):
|
||||||
|
"""
|
||||||
|
Returns the value as number (int if possible, or float otherwise), or
|
||||||
|
raises ``ValueError`` if the specified ``value`` does not have a straight
|
||||||
|
forward numeric conversion.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
fvalue = float(value)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('Not numeric: {}'.format(value))
|
||||||
|
if not math.isnan(fvalue) and not math.isinf(fvalue):
|
||||||
|
ivalue = int(fvalue)
|
||||||
|
if ivalue == fvalue: # yeah, yeah, I know. Whatever. This is best-effort.
|
||||||
|
return ivalue
|
||||||
|
return fvalue
|
||||||
|
|
||||||
|
|
||||||
|
class caseless_string(str):
|
||||||
|
"""
|
||||||
|
Just like built-in Python string except case-insensitive on comparisons. However, the
|
||||||
|
case is preserved otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, basestring):
|
||||||
|
other = other.lower()
|
||||||
|
return self.lower() == other
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __cmp__(self, other):
|
||||||
|
if isinstance(basestring, other):
|
||||||
|
other = other.lower()
|
||||||
|
return cmp(self.lower(), other)
|
||||||
|
|
||||||
|
def format(self, *args, **kwargs):
|
||||||
|
return caseless_string(super(caseless_string, self).format(*args, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
def bitmask(value):
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
value = ranges_to_list(value)
|
||||||
|
if isiterable(value):
|
||||||
|
value = list_to_mask(value)
|
||||||
|
if not isinstance(value, int):
|
||||||
|
raise ValueError(value)
|
||||||
|
return value
|
116
devlib/utils/uboot.py
Normal file
116
devlib/utils/uboot.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
#
|
||||||
|
# Copyright 2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from devlib.utils.serial_port import TIMEOUT
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('U-Boot')
|
||||||
|
|
||||||
|
|
||||||
|
class UbootMenu(object):
|
||||||
|
"""
|
||||||
|
Allows navigating Das U-boot menu over serial (it relies on a pexpect connection).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
option_regex = re.compile(r'^\[(\d+)\]\s+([^\r]+)\r\n', re.M)
|
||||||
|
prompt_regex = re.compile(r'^([^\r\n]+):\s*', re.M)
|
||||||
|
invalid_regex = re.compile(r'Invalid input \(max (\d+)\)', re.M)
|
||||||
|
|
||||||
|
load_delay = 1 # seconds
|
||||||
|
default_timeout = 60 # seconds
|
||||||
|
|
||||||
|
def __init__(self, conn, start_prompt='Hit any key to stop autoboot'):
|
||||||
|
"""
|
||||||
|
:param conn: A serial connection as returned by ``pexect.spawn()``.
|
||||||
|
:param prompt: U-Boot menu prompt
|
||||||
|
:param start_prompt: The starting prompt to wait for during ``open()``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.conn = conn
|
||||||
|
self.conn.crlf = '\n\r' # TODO: this has *got* to be a bug in U-Boot...
|
||||||
|
self.start_prompt = start_prompt
|
||||||
|
self.options = {}
|
||||||
|
self.prompt = None
|
||||||
|
|
||||||
|
def open(self, timeout=default_timeout):
|
||||||
|
"""
|
||||||
|
"Open" the UEFI menu by sending an interrupt on STDIN after seeing the
|
||||||
|
starting prompt (configurable upon creation of the ``UefiMenu`` object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.conn.expect(self.start_prompt, timeout)
|
||||||
|
self.conn.sendline('')
|
||||||
|
time.sleep(self.load_delay)
|
||||||
|
self.conn.readline() # garbage
|
||||||
|
self.conn.sendline('')
|
||||||
|
self.prompt = self.conn.readline().strip()
|
||||||
|
|
||||||
|
def getenv(self):
|
||||||
|
output = self.enter('printenv')
|
||||||
|
result = {}
|
||||||
|
for line in output.split('\n'):
|
||||||
|
if '=' in line:
|
||||||
|
variable, value = line.split('=', 1)
|
||||||
|
result[variable.strip()] = value.strip()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def setenv(self, variable, value, force=False):
|
||||||
|
force_str = ' -f' if force else ''
|
||||||
|
if value is not None:
|
||||||
|
command = 'setenv{} {} {}'.format(force_str, variable, value)
|
||||||
|
else:
|
||||||
|
command = 'setenv{} {}'.format(force_str, variable)
|
||||||
|
return self.enter(command)
|
||||||
|
|
||||||
|
def boot(self):
|
||||||
|
self.write_characters('boot')
|
||||||
|
|
||||||
|
def nudge(self):
|
||||||
|
"""Send a little nudge to ensure there is something to read. This is useful when you're not
|
||||||
|
sure if all out put from the serial has been read already."""
|
||||||
|
self.enter('')
|
||||||
|
|
||||||
|
def enter(self, value, delay=load_delay):
|
||||||
|
"""Like ``select()`` except no resolution is performed -- the value is sent directly
|
||||||
|
to the serial connection."""
|
||||||
|
# Empty the buffer first, so that only response to the input about to
|
||||||
|
# be sent will be processed by subsequent commands.
|
||||||
|
value = str(value)
|
||||||
|
self.empty_buffer()
|
||||||
|
self.write_characters(value)
|
||||||
|
self.conn.expect(self.prompt, timeout=delay)
|
||||||
|
return self.conn.before
|
||||||
|
|
||||||
|
def write_characters(self, line):
|
||||||
|
line = line.rstrip('\r\n')
|
||||||
|
for c in line:
|
||||||
|
self.conn.send(c)
|
||||||
|
time.sleep(0.05)
|
||||||
|
self.conn.sendline('')
|
||||||
|
|
||||||
|
def empty_buffer(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.conn.read_nonblocking(size=1024, timeout=0.1)
|
||||||
|
except TIMEOUT:
|
||||||
|
pass
|
||||||
|
self.conn.buffer = ''
|
||||||
|
|
239
devlib/utils/uefi.py
Normal file
239
devlib/utils/uefi.py
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
# Copyright 2014-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
|
from devlib.utils.serial_port import write_characters, TIMEOUT
|
||||||
|
from devlib.utils.types import boolean
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('UEFI')
|
||||||
|
|
||||||
|
|
||||||
|
class UefiConfig(object):
|
||||||
|
|
||||||
|
def __init__(self, config_dict):
|
||||||
|
if isinstance(config_dict, UefiConfig):
|
||||||
|
self.__dict__ = copy(config_dict.__dict__)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.image_name = config_dict['image_name']
|
||||||
|
self.image_args = config_dict['image_args']
|
||||||
|
self.fdt_support = boolean(config_dict['fdt_support'])
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError('Missing mandatory parameter for UEFI entry config: "{}"'.format(e))
|
||||||
|
self.initrd = config_dict.get('initrd')
|
||||||
|
self.fdt_path = config_dict.get('fdt_path')
|
||||||
|
if self.fdt_path and not self.fdt_support:
|
||||||
|
raise ValueError('FDT path has been specfied for UEFI entry, when FDT support is "False"')
|
||||||
|
|
||||||
|
|
||||||
|
class UefiMenu(object):
|
||||||
|
"""
|
||||||
|
Allows navigating UEFI menu over serial (it relies on a pexpect connection).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
option_regex = re.compile(r'^\[(\d+)\]\s+([^\r]+)\r\n', re.M)
|
||||||
|
prompt_regex = re.compile(r'^(\S[^\r\n]+):\s*', re.M)
|
||||||
|
invalid_regex = re.compile(r'Invalid input \(max (\d+)\)', re.M)
|
||||||
|
|
||||||
|
load_delay = 1 # seconds
|
||||||
|
default_timeout = 60 # seconds
|
||||||
|
|
||||||
|
def __init__(self, conn, prompt='The default boot selection will start in'):
|
||||||
|
"""
|
||||||
|
:param conn: A serial connection as returned by ``pexect.spawn()``.
|
||||||
|
:param prompt: The starting prompt to wait for during ``open()``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.conn = conn
|
||||||
|
self.start_prompt = prompt
|
||||||
|
self.options = {}
|
||||||
|
self.prompt = None
|
||||||
|
self.attempting_invalid_retry = False
|
||||||
|
|
||||||
|
def wait(self, timeout=default_timeout):
|
||||||
|
"""
|
||||||
|
"Open" the UEFI menu by sending an interrupt on STDIN after seeing the
|
||||||
|
starting prompt (configurable upon creation of the ``UefiMenu`` object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.conn.expect(self.start_prompt, timeout)
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def connect(self, timeout=default_timeout):
|
||||||
|
self.nudge()
|
||||||
|
time.sleep(self.load_delay)
|
||||||
|
self.read_menu(timeout=timeout)
|
||||||
|
|
||||||
|
def create_entry(self, name, config):
|
||||||
|
"""Create a new UEFI entry using the parameters. The menu is assumed
|
||||||
|
to be at the top level. Upon return, the menu will be at the top level."""
|
||||||
|
logger.debug('Creating UEFI entry {}'.format(name))
|
||||||
|
self.nudge()
|
||||||
|
self.select('Boot Manager')
|
||||||
|
self.select('Add Boot Device Entry')
|
||||||
|
self.select('NOR Flash')
|
||||||
|
self.enter(config.image_name)
|
||||||
|
self.enter('y' if config.fdt_support else 'n')
|
||||||
|
if config.initrd:
|
||||||
|
self.enter('y')
|
||||||
|
self.enter(config.initrd)
|
||||||
|
else:
|
||||||
|
self.enter('n')
|
||||||
|
self.enter(config.image_args)
|
||||||
|
self.enter(name)
|
||||||
|
|
||||||
|
if config.fdt_path:
|
||||||
|
self.select('Update FDT path')
|
||||||
|
self.enter(config.fdt_path)
|
||||||
|
|
||||||
|
self.select('Return to main menu')
|
||||||
|
|
||||||
|
def delete_entry(self, name):
|
||||||
|
"""Delete the specified UEFI entry. The menu is assumed
|
||||||
|
to be at the top level. Upon return, the menu will be at the top level."""
|
||||||
|
logger.debug('Removing UEFI entry {}'.format(name))
|
||||||
|
self.nudge()
|
||||||
|
self.select('Boot Manager')
|
||||||
|
self.select('Remove Boot Device Entry')
|
||||||
|
self.select(name)
|
||||||
|
self.select('Return to main menu')
|
||||||
|
|
||||||
|
def select(self, option, timeout=default_timeout):
|
||||||
|
"""
|
||||||
|
Select the specified option from the current menu.
|
||||||
|
|
||||||
|
:param option: Could be an ``int`` index of the option, or a string/regex to
|
||||||
|
match option text against.
|
||||||
|
:param timeout: If a non-``int`` option is specified, the option list may need
|
||||||
|
need to be parsed (if it hasn't been already), this may block
|
||||||
|
and the timeout is used to cap that , resulting in a ``TIMEOUT``
|
||||||
|
exception.
|
||||||
|
:param delay: A fixed delay to wait after sending the input to the serial connection.
|
||||||
|
This should be set if input this action is known to result in a
|
||||||
|
long-running operation.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(option, basestring):
|
||||||
|
option = self.get_option_index(option, timeout)
|
||||||
|
self.enter(option)
|
||||||
|
|
||||||
|
def enter(self, value, delay=load_delay):
|
||||||
|
"""Like ``select()`` except no resolution is performed -- the value is sent directly
|
||||||
|
to the serial connection."""
|
||||||
|
# Empty the buffer first, so that only response to the input about to
|
||||||
|
# be sent will be processed by subsequent commands.
|
||||||
|
value = str(value)
|
||||||
|
self._reset()
|
||||||
|
write_characters(self.conn, value)
|
||||||
|
# TODO: in case the value is long an complicated, things may get
|
||||||
|
# screwed up (e.g. there may be line breaks injected), additionally,
|
||||||
|
# special chars might cause regex to fail. To avoid these issues i'm
|
||||||
|
# only matching against the first 5 chars of the value. This is
|
||||||
|
# entirely arbitrary and I'll probably have to find a better way of
|
||||||
|
# doing this at some point.
|
||||||
|
self.conn.expect(value[:5], timeout=delay)
|
||||||
|
time.sleep(self.load_delay)
|
||||||
|
|
||||||
|
def read_menu(self, timeout=default_timeout):
|
||||||
|
"""Parse serial output to get the menu options and the following prompt."""
|
||||||
|
attempting_timeout_retry = False
|
||||||
|
self.attempting_invalid_retry = False
|
||||||
|
while True:
|
||||||
|
index = self.conn.expect([self.option_regex, self.prompt_regex, self.invalid_regex, TIMEOUT],
|
||||||
|
timeout=timeout)
|
||||||
|
match = self.conn.match
|
||||||
|
if index == 0: # matched menu option
|
||||||
|
self.options[match.group(1)] = match.group(2)
|
||||||
|
elif index == 1: # matched prompt
|
||||||
|
self.prompt = match.group(1)
|
||||||
|
break
|
||||||
|
elif index == 2: # matched invalid selection
|
||||||
|
# We've sent an invalid input (which includes an empty line) at
|
||||||
|
# the top-level menu. To get back the menu options, it seems we
|
||||||
|
# need to enter what the error reports as the max + 1, so...
|
||||||
|
if not self.attempting_invalid_retry:
|
||||||
|
self.attempting_invalid_retry = True
|
||||||
|
val = int(match.group(1))
|
||||||
|
self.empty_buffer()
|
||||||
|
self.enter(val)
|
||||||
|
self.select('Return to main menu')
|
||||||
|
self.attempting_invalid_retry = False
|
||||||
|
else: # OK, that didn't work; panic!
|
||||||
|
raise RuntimeError('Could not read menu entries stuck on "{}" prompt'.format(self.prompt))
|
||||||
|
elif index == 3: # timed out
|
||||||
|
if not attempting_timeout_retry:
|
||||||
|
attempting_timeout_retry = True
|
||||||
|
self.nudge()
|
||||||
|
else: # Didn't help. Run away!
|
||||||
|
raise RuntimeError('Did not see a valid UEFI menu.')
|
||||||
|
else:
|
||||||
|
raise AssertionError('Unexpected response waiting for UEFI menu') # should never get here
|
||||||
|
|
||||||
|
def list_options(self, timeout=default_timeout):
|
||||||
|
"""Returns the menu index of the specified option text (uses regex matching). If the option
|
||||||
|
is not in the current menu, ``LookupError`` will be raised."""
|
||||||
|
if not self.prompt:
|
||||||
|
self.read_menu(timeout)
|
||||||
|
return self.options.items()
|
||||||
|
|
||||||
|
def get_option_index(self, text, timeout=default_timeout):
|
||||||
|
"""Returns the menu index of the specified option text (uses regex matching). If the option
|
||||||
|
is not in the current menu, ``LookupError`` will be raised."""
|
||||||
|
if not self.prompt:
|
||||||
|
self.read_menu(timeout)
|
||||||
|
for k, v in self.options.iteritems():
|
||||||
|
if re.search(text, v):
|
||||||
|
return k
|
||||||
|
raise LookupError(text)
|
||||||
|
|
||||||
|
def has_option(self, text, timeout=default_timeout):
|
||||||
|
"""Returns ``True`` if at least one of the options in the current menu has
|
||||||
|
matched (using regex) the specified text."""
|
||||||
|
try:
|
||||||
|
self.get_option_index(text, timeout)
|
||||||
|
return True
|
||||||
|
except LookupError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def nudge(self):
|
||||||
|
"""Send a little nudge to ensure there is something to read. This is useful when you're not
|
||||||
|
sure if all out put from the serial has been read already."""
|
||||||
|
self.enter('')
|
||||||
|
|
||||||
|
def empty_buffer(self):
|
||||||
|
"""Read everything from the serial and clear the internal pexpect buffer. This ensures
|
||||||
|
that the next ``expect()`` call will time out (unless further input will be sent to the
|
||||||
|
serial beforehand. This is used to create a "known" state and avoid unexpected matches."""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.conn.read_nonblocking(size=1024, timeout=0.1)
|
||||||
|
except TIMEOUT:
|
||||||
|
pass
|
||||||
|
self.conn.buffer = ''
|
||||||
|
|
||||||
|
def _reset(self):
|
||||||
|
self.options = {}
|
||||||
|
self.prompt = None
|
||||||
|
self.empty_buffer()
|
||||||
|
|
||||||
|
|
192
doc/Makefile
Normal file
192
doc/Makefile
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# User-friendly check for sphinx-build
|
||||||
|
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||||
|
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
|
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " singlehtml to make a single large HTML file"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " applehelp to make an Apple Help Book"
|
||||||
|
@echo " devhelp to make HTML files and a Devhelp project"
|
||||||
|
@echo " epub to make an epub"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||||
|
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||||
|
@echo " text to make text files"
|
||||||
|
@echo " man to make manual pages"
|
||||||
|
@echo " texinfo to make Texinfo files"
|
||||||
|
@echo " info to make Texinfo files and run them through makeinfo"
|
||||||
|
@echo " gettext to make PO message catalogs"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " xml to make Docutils-native XML files"
|
||||||
|
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
singlehtml:
|
||||||
|
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/devlib.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/devlib.qhc"
|
||||||
|
|
||||||
|
applehelp:
|
||||||
|
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||||
|
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||||
|
"~/Library/Documentation/Help or install it in your application" \
|
||||||
|
"bundle."
|
||||||
|
|
||||||
|
devhelp:
|
||||||
|
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished."
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/devlib"
|
||||||
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/devlib"
|
||||||
|
@echo "# devhelp"
|
||||||
|
|
||||||
|
epub:
|
||||||
|
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
"(use \`make latexpdf' here to do that automatically)."
|
||||||
|
|
||||||
|
latexpdf:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
latexpdfja:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
text:
|
||||||
|
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
|
man:
|
||||||
|
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
|
texinfo:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||||
|
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||||
|
"(use \`make info' here to do that automatically)."
|
||||||
|
|
||||||
|
info:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo "Running Texinfo files through makeinfo..."
|
||||||
|
make -C $(BUILDDIR)/texinfo info
|
||||||
|
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||||
|
|
||||||
|
gettext:
|
||||||
|
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||||
|
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||||
|
@echo "Testing of coverage in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/coverage/python.txt."
|
||||||
|
|
||||||
|
xml:
|
||||||
|
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||||
|
|
||||||
|
pseudoxml:
|
||||||
|
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
287
doc/conf.py
Normal file
287
doc/conf.py
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# devlib documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Tue Aug 11 17:37:27 2015.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its
|
||||||
|
# containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.viewcode',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['static/templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
#source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'devlib'
|
||||||
|
copyright = u'2015, ARM Limited'
|
||||||
|
author = u'ARM Limited'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
version = '0.1'
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = '0.1'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
#today = ''
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
#today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
exclude_patterns = ['../build']
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all
|
||||||
|
# documents.
|
||||||
|
#default_role = None
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
#add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
#add_module_names = True
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
#show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
#modindex_common_prefix = []
|
||||||
|
|
||||||
|
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||||
|
#keep_warnings = False
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
#html_theme_path = []
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
|
# "<project> v<release> documentation".
|
||||||
|
#html_title = None
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
#html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
#html_logo = None
|
||||||
|
|
||||||
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
#html_favicon = None
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['static']
|
||||||
|
|
||||||
|
# Add any extra paths that contain custom files (such as robots.txt or
|
||||||
|
# .htaccess) here, relative to this directory. These files are copied
|
||||||
|
# directly to the root of the documentation.
|
||||||
|
#html_extra_path = []
|
||||||
|
|
||||||
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
|
# using the given strftime format.
|
||||||
|
#html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
#html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
#html_sidebars = {}
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
#html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
#html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
#html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
#html_show_sourcelink = True
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_sphinx = True
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_copyright = True
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
#html_use_opensearch = ''
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
#html_file_suffix = None
|
||||||
|
|
||||||
|
# Language to be used for generating the HTML full-text search index.
|
||||||
|
# Sphinx supports the following languages:
|
||||||
|
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||||
|
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
|
||||||
|
#html_search_language = 'en'
|
||||||
|
|
||||||
|
# A dictionary with options for the search language support, empty by default.
|
||||||
|
# Now only 'ja' uses this config value
|
||||||
|
#html_search_options = {'type': 'default'}
|
||||||
|
|
||||||
|
# The name of a javascript file (relative to the configuration directory) that
|
||||||
|
# implements a search results scorer. If empty, the default will be used.
|
||||||
|
#html_search_scorer = 'scorer.js'
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'devlibdoc'
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'devlib.tex', u'devlib Documentation',
|
||||||
|
u'ARM Limited', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
#latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
#latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
#latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#latex_show_urls = False
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#latex_domain_indices = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(master_doc, 'devlib', u'devlib Documentation',
|
||||||
|
[author], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#man_show_urls = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'devlib', u'devlib Documentation',
|
||||||
|
author, 'devlib', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#texinfo_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#texinfo_domain_indices = True
|
||||||
|
|
||||||
|
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||||
|
#texinfo_show_urls = 'footnote'
|
||||||
|
|
||||||
|
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||||
|
#texinfo_no_detailmenu = False
|
31
doc/index.rst
Normal file
31
doc/index.rst
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.. devlib documentation master file, created by
|
||||||
|
sphinx-quickstart on Tue Aug 11 17:37:27 2015.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
Welcome to devlib documentation
|
||||||
|
===============================
|
||||||
|
|
||||||
|
devlib provides an interface for interacting with remote targets, such as
|
||||||
|
development boards, mobile devices, etc. It also provides means of collecting
|
||||||
|
various measurements and traces from such targets.
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
overview
|
||||||
|
target
|
||||||
|
modules
|
||||||
|
instrumentation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
238
doc/instrumentation.rst
Normal file
238
doc/instrumentation.rst
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
Instrumentation
|
||||||
|
===============
|
||||||
|
|
||||||
|
The ``Instrument`` API provide a consistent way of collecting measurements from
|
||||||
|
a target. Measurements are collected via an instance of a class derived from
|
||||||
|
:class:`Instrument`. An ``Instrument`` allows collection of measurement from one
|
||||||
|
or more channels. An ``Instrument`` may support ``INSTANTANEOUS`` or
|
||||||
|
``CONTINUOUS`` collection, or both.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
The following example shows how to use an instrument to read temperature from an
|
||||||
|
Android target.
|
||||||
|
|
||||||
|
.. code-block:: ipython
|
||||||
|
|
||||||
|
# import and instantiate the Target and the instrument
|
||||||
|
# (note: this assumes exactly one android target connected
|
||||||
|
# to the host machine).
|
||||||
|
In [1]: from devlib import AndroidTarget, HwmonInstrument
|
||||||
|
|
||||||
|
In [2]: t = AndroidTarget()
|
||||||
|
|
||||||
|
In [3]: i = HwmonInstrument(t)
|
||||||
|
|
||||||
|
# Set up the instrument on the Target. In case of HWMON, this is
|
||||||
|
# a no-op, but is included here for completeness.
|
||||||
|
In [4]: i.setup()
|
||||||
|
|
||||||
|
# Find out what the instrument is capable collecting from the
|
||||||
|
# target.
|
||||||
|
In [5]: i.list_channels()
|
||||||
|
Out[5]:
|
||||||
|
[CHAN(battery/temp1, battery_temperature),
|
||||||
|
CHAN(exynos-therm/temp1, exynos-therm_temperature)]
|
||||||
|
|
||||||
|
# Set up a new measurement session, and specify what is to be
|
||||||
|
# collected.
|
||||||
|
In [6]: i.reset(sites=['exynos-therm'])
|
||||||
|
|
||||||
|
# HWMON instrument supports INSTANTANEOUS collection, so invoking
|
||||||
|
# take_measurement() will return a list of measurements take from
|
||||||
|
# each of the channels configured during reset()
|
||||||
|
In [7]: i.take_measurement()
|
||||||
|
Out[7]: [exynos-therm_temperature: 36.0 degrees]
|
||||||
|
|
||||||
|
API
|
||||||
|
---
|
||||||
|
|
||||||
|
Instrument
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
.. class:: Instrument(target, **kwargs)
|
||||||
|
|
||||||
|
An ``Instrument`` allows collection of measurement from one or more
|
||||||
|
channels. An ``Instrument`` may support ``INSTANTANEOUS`` or ``CONTINUOUS``
|
||||||
|
collection, or both.
|
||||||
|
|
||||||
|
.. attribute:: Instrument.mode
|
||||||
|
|
||||||
|
A bit mask that indicates collection modes that are supported by this
|
||||||
|
instrument. Possible values are:
|
||||||
|
|
||||||
|
:INSTANTANEOUS: The instrument supports taking a single sample via
|
||||||
|
``take_measurement()``.
|
||||||
|
:CONTINUOUS: The instrument supports collecting measurements over a
|
||||||
|
period of time via ``start()``, ``stop()``, and
|
||||||
|
``get_data()`` methods.
|
||||||
|
|
||||||
|
.. note:: It's possible for one instrument to support more than a single
|
||||||
|
mode.
|
||||||
|
|
||||||
|
.. attribute:: Instrument.active_channels
|
||||||
|
|
||||||
|
Channels that have been activated via ``reset()``. Measurements will only be
|
||||||
|
collected for these channels.
|
||||||
|
|
||||||
|
.. method:: Instrument.list_channels()
|
||||||
|
|
||||||
|
Returns a list of :class:`InstrumentChannel` instances that describe what
|
||||||
|
this instrument can measure on the current target. A channel is a combination
|
||||||
|
of a ``kind`` of measurement (power, temperature, etc) and a ``site`` that
|
||||||
|
indicates where on the target the measurement will be collected from.
|
||||||
|
|
||||||
|
.. method:: Instrument.get_channels(measure)
|
||||||
|
|
||||||
|
Returns channels for a particular ``measure`` type. A ``measure`` can be
|
||||||
|
either a string (e.g. ``"power"``) or a :class:`MeasurmentType` instance.
|
||||||
|
|
||||||
|
.. method:: Instrument.setup(*args, **kwargs)
|
||||||
|
|
||||||
|
This will set up the instrument on the target. Parameters this method takes
|
||||||
|
are particular to subclasses (see documentation for specific instruments
|
||||||
|
below). What actions are performed by this method are also
|
||||||
|
instrument-specific. Usually these will be things like installing
|
||||||
|
executables, starting services, deploying assets, etc. Typically, this method
|
||||||
|
needs to be invoked at most once per reboot of the target (unless
|
||||||
|
``teardown()`` has been called), but see documentation for the instrument
|
||||||
|
you're interested in.
|
||||||
|
|
||||||
|
.. method:: Instrument.reset([sites, [kinds]])
|
||||||
|
|
||||||
|
This is used to configure an instrument for collection. This must be invoked
|
||||||
|
before ``start()`` is called to begin collection. ``sites`` and ``kinds``
|
||||||
|
parameters may be used to specify which channels measurements should be
|
||||||
|
collected from (if omitted, then measurements will be collected for all
|
||||||
|
available sites/kinds). This methods sets the ``active_channels`` attribute
|
||||||
|
of the ``Instrument``.
|
||||||
|
|
||||||
|
.. method:: Instrument.take_measurment()
|
||||||
|
|
||||||
|
Take a single measurement from ``active_channels``. Returns a list of
|
||||||
|
:class:`Measurement` objects (one for each active channel).
|
||||||
|
|
||||||
|
.. note:: This method is only implemented by :class:`Instrument`\ s that
|
||||||
|
support ``INSTANTANEOUS`` measurment.
|
||||||
|
|
||||||
|
.. method:: Instrument.start()
|
||||||
|
|
||||||
|
Starts collecting measurements from ``active_channels``.
|
||||||
|
|
||||||
|
.. note:: This method is only implemented by :class:`Instrument`\ s that
|
||||||
|
support ``CONTINUOUS`` measurment.
|
||||||
|
|
||||||
|
.. method:: Instrument.stop()
|
||||||
|
|
||||||
|
Stops collecting measurements from ``active_channels``. Must be called after
|
||||||
|
:func:`start()`.
|
||||||
|
|
||||||
|
.. note:: This method is only implemented by :class:`Instrument`\ s that
|
||||||
|
support ``CONTINUOUS`` measurment.
|
||||||
|
|
||||||
|
.. method:: Instrument.get_data(outfile)
|
||||||
|
|
||||||
|
Write collected data into ``outfile``. Must be called after :func:`stop()`.
|
||||||
|
Data will be written in CSV format with a column for each channel and a row
|
||||||
|
for each sample. Column heading will be channel, labels in the form
|
||||||
|
``<site>_<kind>`` (see :class:`InstrumentChannel`). The order of the coluns
|
||||||
|
will be the same as the order of channels in ``Instrument.active_channels``.
|
||||||
|
|
||||||
|
This returns a :class:`MeasurementCsv` instance associated with the outfile
|
||||||
|
that can be used to stream :class:`Measurement`\ s lists (similar to what is
|
||||||
|
returned by ``take_measurement()``.
|
||||||
|
|
||||||
|
.. note:: This method is only implemented by :class:`Instrument`\ s that
|
||||||
|
support ``CONTINUOUS`` measurment.
|
||||||
|
|
||||||
|
|
||||||
|
Instrument Channel
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. class:: InstrumentChannel(name, site, measurement_type, **attrs)
|
||||||
|
|
||||||
|
An :class:`InstrumentChannel` describes a single type of measurement that may
|
||||||
|
be collected by an :class:`Instrument`. A channel is primarily defined by a
|
||||||
|
``site`` and a ``measurement_type``.
|
||||||
|
|
||||||
|
A ``site`` indicates where on the target a measurement is collected from
|
||||||
|
(e.g. a volage rail or location of a sensor).
|
||||||
|
|
||||||
|
A ``measurement_type`` is an instance of :class:`MeasurmentType` that
|
||||||
|
describes what sort of measurment this is (power, temperature, etc). Each
|
||||||
|
mesurement type has a standard unit it is reported in, regardless of an
|
||||||
|
instrument used to collect it.
|
||||||
|
|
||||||
|
A channel (i.e. site/measurement_type combination) is unique per instrument,
|
||||||
|
however there may be more than one channel associated with one site (e.g. for
|
||||||
|
both volatage and power).
|
||||||
|
|
||||||
|
It should not be assumed that any site/measurement_type combination is valid.
|
||||||
|
The list of available channels can queried with
|
||||||
|
:func:`Instrument.list_channels()`.
|
||||||
|
|
||||||
|
.. attribute:: InstrumentChannel.site
|
||||||
|
|
||||||
|
The name of the "site" from which the measurments are collected (e.g. voltage
|
||||||
|
rail, sensor, etc).
|
||||||
|
|
||||||
|
.. attribute:: InstrumentChannel.kind
|
||||||
|
|
||||||
|
A string indingcating the type of measrument that will be collted. This is
|
||||||
|
the ``name`` of the :class:`MeasurmentType` associated with this channel.
|
||||||
|
|
||||||
|
.. attribute:: InstrumentChannel.units
|
||||||
|
|
||||||
|
Units in which measurment will be reported. this is determined by the
|
||||||
|
underlying :class:`MeasurmentType`.
|
||||||
|
|
||||||
|
.. attribute:: InstrumentChannel.label
|
||||||
|
|
||||||
|
A label that can be attached to measurments associated with with channel.
|
||||||
|
This is constructed with ::
|
||||||
|
|
||||||
|
'{}_{}'.format(self.site, self.kind)
|
||||||
|
|
||||||
|
|
||||||
|
Measurement Types
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In order to make instruments easer to use, and to make it easier to swap them
|
||||||
|
out when necessary (e.g. change method of collecting power), a number of
|
||||||
|
standard measurement types are defined. This way, for example, power will always
|
||||||
|
be reported as "power" in Watts, and never as "pwr" in milliWatts. Currently
|
||||||
|
defined measurement types are
|
||||||
|
|
||||||
|
|
||||||
|
+-------------+---------+---------------+
|
||||||
|
| name | units | category |
|
||||||
|
+=============+=========+===============+
|
||||||
|
| time | seconds | |
|
||||||
|
+-------------+---------+---------------+
|
||||||
|
| temperature | degrees | |
|
||||||
|
+-------------+---------+---------------+
|
||||||
|
| power | watts | power/energy |
|
||||||
|
+-------------+---------+---------------+
|
||||||
|
| voltage | volts | power/energy |
|
||||||
|
+-------------+---------+---------------+
|
||||||
|
| current | amps | power/energy |
|
||||||
|
+-------------+---------+---------------+
|
||||||
|
| energy | joules | power/energy |
|
||||||
|
+-------------+---------+---------------+
|
||||||
|
| tx | bytes | data transfer |
|
||||||
|
+-------------+---------+---------------+
|
||||||
|
| rx | bytes | data transfer |
|
||||||
|
+-------------+---------+---------------+
|
||||||
|
| tx/rx | bytes | data transfer |
|
||||||
|
+-------------+---------+---------------+
|
||||||
|
|
||||||
|
|
||||||
|
.. instruments:
|
||||||
|
|
||||||
|
Available Instruments
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
This section lists instruments that are currently part of devlib.
|
||||||
|
|
||||||
|
TODO
|
263
doc/make.bat
Normal file
263
doc/make.bat
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set BUILDDIR=_build
|
||||||
|
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||||
|
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||||
|
if NOT "%PAPER%" == "" (
|
||||||
|
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||||
|
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
if "%1" == "help" (
|
||||||
|
:help
|
||||||
|
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||||
|
echo. html to make standalone HTML files
|
||||||
|
echo. dirhtml to make HTML files named index.html in directories
|
||||||
|
echo. singlehtml to make a single large HTML file
|
||||||
|
echo. pickle to make pickle files
|
||||||
|
echo. json to make JSON files
|
||||||
|
echo. htmlhelp to make HTML files and a HTML help project
|
||||||
|
echo. qthelp to make HTML files and a qthelp project
|
||||||
|
echo. devhelp to make HTML files and a Devhelp project
|
||||||
|
echo. epub to make an epub
|
||||||
|
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||||
|
echo. text to make text files
|
||||||
|
echo. man to make manual pages
|
||||||
|
echo. texinfo to make Texinfo files
|
||||||
|
echo. gettext to make PO message catalogs
|
||||||
|
echo. changes to make an overview over all changed/added/deprecated items
|
||||||
|
echo. xml to make Docutils-native XML files
|
||||||
|
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||||
|
echo. linkcheck to check all external links for integrity
|
||||||
|
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||||
|
echo. coverage to run coverage check of the documentation if enabled
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "clean" (
|
||||||
|
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||||
|
del /q /s %BUILDDIR%\*
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
REM Check if sphinx-build is available and fallback to Python version if any
|
||||||
|
%SPHINXBUILD% 2> nul
|
||||||
|
if errorlevel 9009 goto sphinx_python
|
||||||
|
goto sphinx_ok
|
||||||
|
|
||||||
|
:sphinx_python
|
||||||
|
|
||||||
|
set SPHINXBUILD=python -m sphinx.__init__
|
||||||
|
%SPHINXBUILD% 2> nul
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.http://sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:sphinx_ok
|
||||||
|
|
||||||
|
|
||||||
|
if "%1" == "html" (
|
||||||
|
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "dirhtml" (
|
||||||
|
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "singlehtml" (
|
||||||
|
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "pickle" (
|
||||||
|
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the pickle files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "json" (
|
||||||
|
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the JSON files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "htmlhelp" (
|
||||||
|
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||||
|
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "qthelp" (
|
||||||
|
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||||
|
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||||
|
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\devlib.qhcp
|
||||||
|
echo.To view the help file:
|
||||||
|
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\devlib.ghc
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "devhelp" (
|
||||||
|
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "epub" (
|
||||||
|
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latex" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latexpdf" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
cd %BUILDDIR%/latex
|
||||||
|
make all-pdf
|
||||||
|
cd %~dp0
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latexpdfja" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
cd %BUILDDIR%/latex
|
||||||
|
make all-pdf-ja
|
||||||
|
cd %~dp0
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "text" (
|
||||||
|
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "man" (
|
||||||
|
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "texinfo" (
|
||||||
|
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "gettext" (
|
||||||
|
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "changes" (
|
||||||
|
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.The overview file is in %BUILDDIR%/changes.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "linkcheck" (
|
||||||
|
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Link check complete; look for any errors in the above output ^
|
||||||
|
or in %BUILDDIR%/linkcheck/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "doctest" (
|
||||||
|
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Testing of doctests in the sources finished, look at the ^
|
||||||
|
results in %BUILDDIR%/doctest/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "coverage" (
|
||||||
|
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Testing of coverage in the sources finished, look at the ^
|
||||||
|
results in %BUILDDIR%/coverage/python.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "xml" (
|
||||||
|
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "pseudoxml" (
|
||||||
|
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
:end
|
172
doc/modules.rst
Normal file
172
doc/modules.rst
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
Modules
|
||||||
|
=======
|
||||||
|
|
||||||
|
Modules add additional functionality to the core :class:`Target` interface.
|
||||||
|
Usually, it is support for specific subsystems on the target. Modules are
|
||||||
|
instantiated as attributes of the :class:`Target` instance.
|
||||||
|
|
||||||
|
hotplug
|
||||||
|
-------
|
||||||
|
|
||||||
|
Kernel ``hotplug`` subsystem allows offlining ("removing") cores from the
|
||||||
|
system, and onlining them back int. The ``devlib`` module exposes a simple
|
||||||
|
interface to this subsystem
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from devlib import LocalLinuxTarget
|
||||||
|
target = LocalLinuxTarget()
|
||||||
|
|
||||||
|
# offline cpus 2 and 3, "removing" them from the system
|
||||||
|
target.hotplug.offline(2, 3)
|
||||||
|
|
||||||
|
# bring CPU 2 back in
|
||||||
|
target.hotplug.online(2)
|
||||||
|
|
||||||
|
# Make sure all cpus are online
|
||||||
|
target.hotplug.online_all()
|
||||||
|
|
||||||
|
cpufreq
|
||||||
|
-------
|
||||||
|
|
||||||
|
``cpufreq`` is the kernel subsystem for managing DVFS (Dynamic Voltage and
|
||||||
|
Frequency Scaling). It allows controlling frequency ranges and switching
|
||||||
|
policies (governors). The ``devlib`` module exposes the following interface
|
||||||
|
|
||||||
|
.. note:: On ARM big.LITTLE systems, all cores on a cluster (usually all cores
|
||||||
|
of the same type) are in the same frequency domain, so setting
|
||||||
|
``cpufreq`` state on one core on a cluter will affect all cores on
|
||||||
|
that cluster. Because of this, some devices only expose cpufreq sysfs
|
||||||
|
interface (which is what is used by the ``devlib`` module) on the
|
||||||
|
first cpu in a cluster. So to keep your scripts proable, always use
|
||||||
|
the fist (online) CPU in a cluster to set ``cpufreq`` state.
|
||||||
|
|
||||||
|
.. method:: target.cpufreq.list_governors(cpu)
|
||||||
|
|
||||||
|
List cpufreq governors available for the specified cpu. Returns a list of
|
||||||
|
strings.
|
||||||
|
|
||||||
|
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
|
||||||
|
``1`` or ``"cpu1"``).
|
||||||
|
|
||||||
|
.. method:: target.cpufreq.list_governor_tunables(cpu)
|
||||||
|
|
||||||
|
List the tunables for the specified cpu's current governor.
|
||||||
|
|
||||||
|
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
|
||||||
|
``1`` or ``"cpu1"``).
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: target.cpufreq.get_governor(cpu)
|
||||||
|
|
||||||
|
Returns the name of the currently set governor for the specified cpu.
|
||||||
|
|
||||||
|
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
|
||||||
|
``1`` or ``"cpu1"``).
|
||||||
|
|
||||||
|
.. method:: target.cpufreq.set_governor(cpu, governor, **kwargs)
|
||||||
|
|
||||||
|
Sets the governor for the specified cpu.
|
||||||
|
|
||||||
|
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
|
||||||
|
``1`` or ``"cpu1"``).
|
||||||
|
:param governor: The name of the governor. This must be one of the governors
|
||||||
|
supported by the CPU (as retrunted by ``list_governors()``.
|
||||||
|
|
||||||
|
Keyword arguments may be used to specify governor tunable values.
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: target.cpufreq.get_governor_tunables(cpu)
|
||||||
|
|
||||||
|
Return a dict with the values of the specfied CPU's current governor.
|
||||||
|
|
||||||
|
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
|
||||||
|
``1`` or ``"cpu1"``).
|
||||||
|
|
||||||
|
.. method:: target.cpufreq.set_governor_tunables(cpu, **kwargs)
|
||||||
|
|
||||||
|
Set the tunables for the current governor on the specified CPU.
|
||||||
|
|
||||||
|
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
|
||||||
|
``1`` or ``"cpu1"``).
|
||||||
|
|
||||||
|
Keyword arguments should be used to specify tunable values.
|
||||||
|
|
||||||
|
.. method:: target.cpufreq.list_frequencie(cpu)
|
||||||
|
|
||||||
|
List DVFS frequencies supported by the specified CPU. Returns a list of ints.
|
||||||
|
|
||||||
|
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
|
||||||
|
``1`` or ``"cpu1"``).
|
||||||
|
|
||||||
|
.. method:: target.cpufreq.get_min_frequency(cpu)
|
||||||
|
target.cpufreq.get_max_frequency(cpu)
|
||||||
|
target.cpufreq.set_min_frequency(cpu, frequency[, exact=True])
|
||||||
|
target.cpufreq.set_max_frequency(cpu, frequency[, exact=True])
|
||||||
|
|
||||||
|
Get and set min and max frequencies on the specfied CPU. "set" functions are
|
||||||
|
avialable with all governors other than ``userspace``.
|
||||||
|
|
||||||
|
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
|
||||||
|
``1`` or ``"cpu1"``).
|
||||||
|
:param frequency: Frequency to set.
|
||||||
|
|
||||||
|
.. method:: target.cpufreq.get_frequency(cpu)
|
||||||
|
target.cpufreq.set_frequency(cpu, frequency[, exact=True])
|
||||||
|
|
||||||
|
Get and set current frequency on the specified CPU. ``set_frequency`` is only
|
||||||
|
available if the current governor is ``userspace``.
|
||||||
|
|
||||||
|
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
|
||||||
|
``1`` or ``"cpu1"``).
|
||||||
|
:param frequency: Frequency to set.
|
||||||
|
|
||||||
|
cpuidle
|
||||||
|
-------
|
||||||
|
|
||||||
|
``cpufreq`` is the kernel subsystem for managing CPU low power (idle) states.
|
||||||
|
|
||||||
|
.. method:: taget.cpuidle.get_driver()
|
||||||
|
|
||||||
|
Return the name current cpuidle driver.
|
||||||
|
|
||||||
|
.. method:: taget.cpuidle.get_governor()
|
||||||
|
|
||||||
|
Return the name current cpuidle governor (policy).
|
||||||
|
|
||||||
|
.. method:: target.cpuidle.get_states([cpu=0])
|
||||||
|
|
||||||
|
Return idle states (optionally, for the specified CPU). Returns a list of
|
||||||
|
:class:`CpuidleState` instances.
|
||||||
|
|
||||||
|
.. method:: target.cpuidle.get_state(state[, cpu=0])
|
||||||
|
|
||||||
|
Return :class:`CpuidleState` instance (optionally, for the specified CPU)
|
||||||
|
representing the specified idle state. ``state`` can be either an integer
|
||||||
|
index of the state or a string with the states ``name`` or ``desc``.
|
||||||
|
|
||||||
|
.. method:: target.cpuidle.enable(state[, cpu=0])
|
||||||
|
target.cpuidle.disable(state[, cpu=0])
|
||||||
|
target.cpuidle.enable_all([cpu=0])
|
||||||
|
target.cpuidle.disable_all([cpu=0])
|
||||||
|
|
||||||
|
Enable or disable the specified or all states (optionally on the specified
|
||||||
|
CPU.
|
||||||
|
|
||||||
|
You can also call ``enable()`` or ``disable()`` on :class:`CpuidleState` objects
|
||||||
|
returned by get_state(s).
|
||||||
|
|
||||||
|
cgroups
|
||||||
|
-------
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
hwmon
|
||||||
|
-----
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
API
|
||||||
|
---
|
||||||
|
|
||||||
|
TODO
|
282
doc/overview.rst
Normal file
282
doc/overview.rst
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
A :class:`Target` instance serves as the main interface to the target device.
|
||||||
|
There currently three target interfaces:
|
||||||
|
|
||||||
|
- :class:`LinuxTarget` for interacting with Linux devices over SSH.
|
||||||
|
- :class:`AndroidTraget` for interacting with Android devices over adb.
|
||||||
|
- :class:`LocalLinuxTarget`: for interacting with the local Linux host.
|
||||||
|
|
||||||
|
They all work in more-or-less the same way, with the major difference being in
|
||||||
|
how connection settings are specified; though there may also be a few APIs
|
||||||
|
specific to a particular target type (e.g. :class:`AndroidTarget` exposes
|
||||||
|
methods for working with logcat).
|
||||||
|
|
||||||
|
|
||||||
|
Acquiring a Target
|
||||||
|
------------------
|
||||||
|
|
||||||
|
To create an interface to your device, you just need to instantiate one of the
|
||||||
|
:class:`Target` derivatives listed above, and pass it the right
|
||||||
|
``connection_settings``. Code snippet below gives a typical example of
|
||||||
|
instantiating each of the three target types.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from devlib import LocalLinuxTarget, LinuxTarget, AndroidTarget
|
||||||
|
|
||||||
|
# Local machine requires no special connection settings.
|
||||||
|
t1 = LocalLinuxTarget()
|
||||||
|
|
||||||
|
# For a Linux device, you will need to provide the normal SSH credentials.
|
||||||
|
# Both password-based, and key-based authentication is supported (password
|
||||||
|
# authentication requires sshpass to be installed on your host machine).'
|
||||||
|
t2 = LinuxTarget(connetion_settings={'host': '192.168.0.5',
|
||||||
|
'username': 'root',
|
||||||
|
'password': 'sekrit',
|
||||||
|
# or
|
||||||
|
'keyfile': '/home/me/.ssh/id_rsa'})
|
||||||
|
|
||||||
|
# For an Android target, you will need to pass the device name as reported
|
||||||
|
# by "adb devices". If there is only one device visible to adb, you can omit
|
||||||
|
# this setting and instantiate similar to a local target.
|
||||||
|
t3 = AndroidTarget(connection_settings={'device': '0123456789abcde'})
|
||||||
|
|
||||||
|
Instantiating a target may take a second or two as the remote device will be
|
||||||
|
queried to initialize :class:`Target`'s internal state. If you would like to
|
||||||
|
create a :class:`Target` instance but not immediately connect to the remote
|
||||||
|
device, you can pass ``connect=False`` parameter. If you do that, you would have
|
||||||
|
to then explicitly call ``t.connect()`` before you can interact with the device.
|
||||||
|
|
||||||
|
There are a few additional parameters you can pass in instantiation besides
|
||||||
|
``connection_settings``, but they are usually unnecessary. Please see
|
||||||
|
:class:`Target` API documentation for more details.
|
||||||
|
|
||||||
|
Target Interface
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This is a quick overview of the basic interface to the device. See
|
||||||
|
:class:`Targeet` API documentation for the full list of supported methods and
|
||||||
|
more detailed documentation.
|
||||||
|
|
||||||
|
One-time Setup
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from devlib import LocalLinuxTarget
|
||||||
|
t = LocalLinuxTarget()
|
||||||
|
|
||||||
|
t.setup()
|
||||||
|
|
||||||
|
This sets up the target for ``devlib`` interaction. This includes creating
|
||||||
|
working directories, deploying busybox, etc. It's usually enough to do this once
|
||||||
|
for a new device, as the changes this makes will persist across reboots.
|
||||||
|
However, there is no issue with calling this multiple times, so, to be on the
|
||||||
|
safe site, it's a good idea to call this once at the beginning of your scripts.
|
||||||
|
|
||||||
|
Command Execution
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
There are several ways to execute a command on the target. In each case, a
|
||||||
|
:class:`TargetError` will be raised if something goes wrong. In very case, it is
|
||||||
|
also possible to specify ``as_root=True`` if the specified command should be
|
||||||
|
executed as root.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from devlib import LocalLinuxTarget
|
||||||
|
t = LocalLinuxTarget()
|
||||||
|
|
||||||
|
# Execute a command
|
||||||
|
output = t.execute('echo $PWD')
|
||||||
|
|
||||||
|
# Execute command via a subprocess and return the corresponding Popen object.
|
||||||
|
# This will block current connection to the device until the command
|
||||||
|
# completes.
|
||||||
|
p = t.background('echo $PWD')
|
||||||
|
output, error = p.communicate()
|
||||||
|
|
||||||
|
# Run the command in the background on the device and return immediately.
|
||||||
|
# This will not block the connection, allowing to immediately execute another
|
||||||
|
# command.
|
||||||
|
t.kick_off('echo $PWD')
|
||||||
|
|
||||||
|
# This is used to invoke an executable binary on the device. This allows some
|
||||||
|
# finer-grained control over the invocation, such as specifying the directory
|
||||||
|
# in which the executable will run; however you're limited to a single binary
|
||||||
|
# and cannot construct complex commands (e.g. this does not allow chaining or
|
||||||
|
# piping several commands together).
|
||||||
|
output = t.invoke('echo', args=['$PWD'], in_directory='/')
|
||||||
|
|
||||||
|
File Transfer
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from devlib import LocalLinuxTarget
|
||||||
|
t = LocalLinuxTarget()
|
||||||
|
|
||||||
|
# "push" a file from the local machine onto the target device.
|
||||||
|
t.push('/path/to/local/file.txt', '/path/to/target/file.txt')
|
||||||
|
|
||||||
|
# "pull" a file from the target device into a location on the local machine
|
||||||
|
t.pull('/path/to/target/file.txt', '/path/to/local/file.txt')
|
||||||
|
|
||||||
|
# Install the specified binary on the target. This will deploy the file and
|
||||||
|
# ensure it's executable. This will *not* guarantee that the binary will be
|
||||||
|
# in PATH. Instead the path to the binary will be returned; this should be
|
||||||
|
# used to call the binary henceforth.
|
||||||
|
target_bin = t.install('/path/to/local/bin.exe')
|
||||||
|
# Example invocation:
|
||||||
|
output = t.execute('{} --some-option'.format(target_bin))
|
||||||
|
|
||||||
|
The usual access permission constraints on the user account (both on the target
|
||||||
|
and the host) apply.
|
||||||
|
|
||||||
|
Process Control
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import signal
|
||||||
|
from devlib import LocalLinuxTarget
|
||||||
|
t = LocalLinuxTarget()
|
||||||
|
|
||||||
|
# return PIDs of all running instances of a process
|
||||||
|
pids = t.get_pids_of('sshd')
|
||||||
|
|
||||||
|
# kill a running process. This works the same ways as the kill command, so
|
||||||
|
# SIGTERM will be used by default.
|
||||||
|
t.kill(666, signal=signal.SIGKILL)
|
||||||
|
|
||||||
|
# kill all running instances of a process.
|
||||||
|
t.killall('badexe', signal=signal.SIGKILL)
|
||||||
|
|
||||||
|
# List processes running on the target. This retruns a list of parsed
|
||||||
|
# PsEntry records.
|
||||||
|
entries = t.ps()
|
||||||
|
# e.g. print virtual memory sizes of all running sshd processes:
|
||||||
|
print ', '.join(str(e.vsize) for e in entries if e.name == 'sshd')
|
||||||
|
|
||||||
|
|
||||||
|
More...
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
As mentioned previously, the above is not intended to be exhaustive
|
||||||
|
documentation of the :class:`Target` interface. Please refer to the API
|
||||||
|
documentation for the full list of attributes and methods and their parameters.
|
||||||
|
|
||||||
|
Super User Privileges
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
It is not necessary for the account logged in on the target to have super user
|
||||||
|
privileges, however the functionality will obviously be diminished, if that is
|
||||||
|
not the case. ``devilib`` will determine if the logged in user has root
|
||||||
|
privileges and the correct way to invoke it. You should avoid including "sudo"
|
||||||
|
directly in your commands, instead, specify ``as_root=True`` where needed. This
|
||||||
|
will make your scripts portable across multiple devices and OS's.
|
||||||
|
|
||||||
|
|
||||||
|
On-Target Locations
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
File system layouts vary wildly between devices and operating systems.
|
||||||
|
Hard-coding absolute paths in your scripts will mean there is a good chance they
|
||||||
|
will break if run on a different device. To help with this, ``devlib`` defines
|
||||||
|
a couple of "standard" locations and a means of working with them.
|
||||||
|
|
||||||
|
working_directory
|
||||||
|
This is a directory on the target readable and writable by the account
|
||||||
|
used to log in. This should generally be used for all output generated
|
||||||
|
by your script on the device and as the destination for all
|
||||||
|
host-to-target file transfers. It may or may not permit execution so
|
||||||
|
executables should not be run directly from here.
|
||||||
|
|
||||||
|
executables_directory
|
||||||
|
This directory allows execution. This will be used by ``install()``.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from devlib import LocalLinuxTarget
|
||||||
|
t = LocalLinuxTarget()
|
||||||
|
|
||||||
|
# t.path is equivalent to Python standard library's os.path, and should be
|
||||||
|
# used in the same way. This insures that your scripts are portable across
|
||||||
|
# both target and host OS variations. e.g.
|
||||||
|
on_target_path = t.path.join(t.working_directory, 'assets.tar.gz')
|
||||||
|
t.push('/local/path/to/assets.tar.gz', on_target_path)
|
||||||
|
|
||||||
|
# Since working_directory is a common base path for on-target locations,
|
||||||
|
# there a short-hand for the above:
|
||||||
|
t.push('/local/path/to/assets.tar.gz', t.get_workpath('assets.tar.gz'))
|
||||||
|
|
||||||
|
|
||||||
|
Modules
|
||||||
|
-------
|
||||||
|
|
||||||
|
Additional functionality is exposed via modules. Modules are initialized as
|
||||||
|
attributes of a target instance. By default, ``hotplug``, ``cpufreq``,
|
||||||
|
``cpuidle``, ``cgroups`` and ``hwmon`` will attempt to load on target; additional
|
||||||
|
modules may be specified when creating a :class:`Target` instance.
|
||||||
|
|
||||||
|
A module will probe the target for support before attempting to load. So if the
|
||||||
|
underlying platform does not support particular functionality (e.g. the kernel
|
||||||
|
on target device was built without hotplug support). To check whether a module
|
||||||
|
has been successfully installed on a target, you can use ``has()`` method, e.g.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from devlib import LocalLinuxTarget
|
||||||
|
t = LocalLinuxTarget()
|
||||||
|
|
||||||
|
cpu0_freqs = []
|
||||||
|
if t.has('cpufreq'):
|
||||||
|
cpu0_freqs = t.cpufreq.list_frequencies(0)
|
||||||
|
|
||||||
|
|
||||||
|
Please see the modules documentation for more detail.
|
||||||
|
|
||||||
|
|
||||||
|
Measurement and Trace
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
You can collected traces (currently, just ftrace) using
|
||||||
|
:class:`TraceCollector`\ s. For example
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from devlib import AndroidTarget, FtraceCollector
|
||||||
|
t = LocalLinuxTarget()
|
||||||
|
|
||||||
|
# Initialize a collector specifying the events you want to collect and
|
||||||
|
# the buffer size to be used.
|
||||||
|
trace = FtraceCollector(t, events=['power*'], buffer_size=40000)
|
||||||
|
|
||||||
|
# clear ftrace buffer
|
||||||
|
trace.reset()
|
||||||
|
|
||||||
|
# start trace collection
|
||||||
|
trace.start()
|
||||||
|
|
||||||
|
# Perform the operations you want to trace here...
|
||||||
|
import time; time.sleep(5)
|
||||||
|
|
||||||
|
# stop trace collection
|
||||||
|
trace.stop()
|
||||||
|
|
||||||
|
# extract the trace file from the target into a local file
|
||||||
|
trace.get_trace('/tmp/trace.bin')
|
||||||
|
|
||||||
|
# View trace file using Kernelshark (must be installed on the host).
|
||||||
|
trace.view('/tmp/trace.bin')
|
||||||
|
|
||||||
|
# Convert binary trace into text format. This would normally be done
|
||||||
|
# automatically during get_trace(), unless autoreport is set to False during
|
||||||
|
# instantiation of the trace collector.
|
||||||
|
trace.report('/tmp/trace.bin', '/tmp/trace.txt')
|
||||||
|
|
||||||
|
In a similar way, :class:`Instrument` instances may be used to collect
|
||||||
|
measurements (such as power) from targets that support it. Please see
|
||||||
|
instruments documentation for more details.
|
422
doc/target.rst
Normal file
422
doc/target.rst
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
Target
|
||||||
|
======
|
||||||
|
|
||||||
|
|
||||||
|
.. class:: Target(connection_settings=None, platform=None, working_directory=None, executables_directory=None, connect=True, modules=None, load_default_modules=True, shell_prompt=DEFAULT_SHELL_PROMPT)
|
||||||
|
|
||||||
|
:class:`Target` is the primary interface to the remote device. All interactions
|
||||||
|
with the device are performed via a :class:`Target` instance, either
|
||||||
|
directly, or via its modules or a wrapper interface (such as an
|
||||||
|
:class:`Instrument`).
|
||||||
|
|
||||||
|
:param connection_settings: A ``dict`` that specifies how to connect to the remote
|
||||||
|
device. Its contents depend on the specific :class:`Target` type used (e.g.
|
||||||
|
:class:`AndroidTarget` expects the adb ``device`` name).
|
||||||
|
|
||||||
|
:param platform: A :class:`Target` defines interactions at Operating System level. A
|
||||||
|
:class:`Platform` describes the underlying hardware (such as CPUs
|
||||||
|
available). If a :class:`Platform` instance is not specified on
|
||||||
|
:class:`Target` creation, one will be created automatically and it will
|
||||||
|
dynamically probe the device to discover as much about the underlying
|
||||||
|
hardware as it can.
|
||||||
|
|
||||||
|
:param working_directory: This is primary location for on-target file system
|
||||||
|
interactions performed by ``devlib``. This location *must* be readable and
|
||||||
|
writable directly (i.e. without sudo) by the connection's user account.
|
||||||
|
It may or may not allow execution. This location will be created,
|
||||||
|
if necessary, during ``setup()``.
|
||||||
|
|
||||||
|
If not explicitly specified, this will be set to a default value
|
||||||
|
depending on the type of :class:`Target`
|
||||||
|
|
||||||
|
:param executables_directory: This is the location to which ``devlib`` will
|
||||||
|
install executable binaries (either during ``setup()`` or via an
|
||||||
|
explicit ``install()`` call). This location *must* support execution
|
||||||
|
(obviously). It should also be possible to write to this location,
|
||||||
|
possibly with elevated privileges (i.e. on a rooted Linux target, it
|
||||||
|
should be possible to write here with sudo, but not necessarily directly
|
||||||
|
by the connection's account). This location will be created,
|
||||||
|
if necessary, during ``setup()``.
|
||||||
|
|
||||||
|
This location does *not* to be same as the system's executables
|
||||||
|
location. In fact, to prevent devlib from overwriting system's defaults,
|
||||||
|
it better if this is a separate location, if possible.
|
||||||
|
|
||||||
|
If not explicitly specified, this will be set to a default value
|
||||||
|
depending on the type of :class:`Target`
|
||||||
|
|
||||||
|
:param connect: Specifies whether a connections should be established to the
|
||||||
|
target. If this is set to ``False``, then ``connect()`` must be
|
||||||
|
explicitly called later on before the :class:`Target` instance can be
|
||||||
|
used.
|
||||||
|
|
||||||
|
:param modules: a list of additional modules to be installed. Some modules will
|
||||||
|
try to install by default (if supported by the underlying target).
|
||||||
|
Current default modules are ``hotplug``, ``cpufreq``, ``cpuidle``,
|
||||||
|
``cgroups``, and ``hwmon``.
|
||||||
|
|
||||||
|
See modules documentation for more detail.
|
||||||
|
|
||||||
|
:param load_default_modules: If set to ``False``, default modules listed
|
||||||
|
above will *not* attempt to load. This may be used to either speed up
|
||||||
|
target instantiation (probing for initializing modules takes a bit of time)
|
||||||
|
or if there is an issue with one of the modules on a particular device
|
||||||
|
(the rest of the modules will then have to be explicitly specified in
|
||||||
|
the ``modules``).
|
||||||
|
|
||||||
|
:param shell_prompt: This is a regular expression that matches the shell
|
||||||
|
prompted on the target. This may be used by some modules that establish
|
||||||
|
auxiliary connections to a target over UART.
|
||||||
|
|
||||||
|
.. attribute:: Target.core_names
|
||||||
|
|
||||||
|
This is a list containing names of CPU cores on the target, in the order in
|
||||||
|
which they are index by the kernel. This is obtained via the underlying
|
||||||
|
:class:`Platform`.
|
||||||
|
|
||||||
|
.. attribute:: Target.core_clusters
|
||||||
|
|
||||||
|
Some devices feature heterogeneous core configurations (such as ARM
|
||||||
|
big.LITTLE). This is a list that maps CPUs onto underlying clusters.
|
||||||
|
(Usually, but not always, clusters correspond to groups of CPUs with the same
|
||||||
|
name). This is obtained via the underlying :class:`Platform`.
|
||||||
|
|
||||||
|
.. attribute:: Target.big_core
|
||||||
|
|
||||||
|
This is the name of the cores that the "big"s in an ARM big.LITTLE
|
||||||
|
configuration. This is obtained via the underlying :class:`Platform`.
|
||||||
|
|
||||||
|
.. attribute:: Target.little_core
|
||||||
|
|
||||||
|
This is the name of the cores that the "little"s in an ARM big.LITTLE
|
||||||
|
configuration. This is obtained via the underlying :class:`Platform`.
|
||||||
|
|
||||||
|
.. attribute:: Target.is_connected
|
||||||
|
|
||||||
|
A boolean value that indicates whether an active connection exists to the
|
||||||
|
target device.
|
||||||
|
|
||||||
|
.. attribute:: Target.connected_as_root
|
||||||
|
|
||||||
|
A boolean value that indicate whether the account that was used to connect to
|
||||||
|
the target device is "root" (uid=0).
|
||||||
|
|
||||||
|
.. attribute:: Target.is_rooted
|
||||||
|
|
||||||
|
A boolean value that indicates whether the connected user has super user
|
||||||
|
privileges on the devices (either is root, or is a sudoer).
|
||||||
|
|
||||||
|
.. attribute:: Target.kernel_version
|
||||||
|
|
||||||
|
The version of the kernel on the target device. This returns a
|
||||||
|
:class:`KernelVersion` instance that has separate ``version`` and ``release``
|
||||||
|
fields.
|
||||||
|
|
||||||
|
.. attribute:: Target.os_version
|
||||||
|
|
||||||
|
This is a dict that contains a mapping of OS version elements to their
|
||||||
|
values. This mapping is OS-specific.
|
||||||
|
|
||||||
|
.. attribute:: Target.cpuinfo
|
||||||
|
|
||||||
|
This is a :class:`Cpuinfo` instance which contains parsed contents of
|
||||||
|
``/proc/cpuinfo``.
|
||||||
|
|
||||||
|
.. attribute:: Target.number_of_cpus
|
||||||
|
|
||||||
|
The total number of CPU cores on the target device.
|
||||||
|
|
||||||
|
.. attribute:: Target.config
|
||||||
|
|
||||||
|
A :class:`KernelConfig` instance that contains parsed kernel config from the
|
||||||
|
target device. This may be ``None`` if kernel config could not be extracted.
|
||||||
|
|
||||||
|
.. attribute:: Target.user
|
||||||
|
|
||||||
|
The name of the user logged in on the target device.
|
||||||
|
|
||||||
|
.. attribute:: Target.conn
|
||||||
|
|
||||||
|
The underlying connection object. This will be ``None`` if an active
|
||||||
|
connection does not exist (e.g. if ``connect=False`` as passed on
|
||||||
|
initialization and ``connect()`` has not been called).
|
||||||
|
|
||||||
|
.. note:: a :class:`Target` will automatically create a connection per
|
||||||
|
thread. This will always be set to the connection for the current
|
||||||
|
thread.
|
||||||
|
|
||||||
|
.. method:: Target.connect([timeout])
|
||||||
|
|
||||||
|
Establish a connection to the target. It is usually not necessary to call
|
||||||
|
this explicitly, as a connection gets automatically established on
|
||||||
|
instantiation.
|
||||||
|
|
||||||
|
.. method:: Target.disconnect()
|
||||||
|
|
||||||
|
Disconnect from target, closing all active connections to it.
|
||||||
|
|
||||||
|
.. method:: Target.get_connection([timeout])
|
||||||
|
|
||||||
|
Get an additional connection to the target. A connection can be used to
|
||||||
|
execute one blocking command at time. This will return a connection that can
|
||||||
|
be used to interact with a target in parallel while a blocking operation is
|
||||||
|
being executed.
|
||||||
|
|
||||||
|
This should *not* be used to establish an initial connection; use
|
||||||
|
``connect()`` instead.
|
||||||
|
|
||||||
|
.. note:: :class:`Target` will automatically create a connection per
|
||||||
|
thread, so you don't normally need to use this explicitly in
|
||||||
|
threaded code. This is generally useful if you want to perform a
|
||||||
|
blocking operation (e.g. using ``background()``) while at the same
|
||||||
|
time doing something else in the same host-side thread.
|
||||||
|
|
||||||
|
.. method:: Target.setup([executables])
|
||||||
|
|
||||||
|
This will perform an initial one-time set up of a device for devlib
|
||||||
|
interaction. This involves deployment of tools relied on the :class:`Target`,
|
||||||
|
creation of working locations on the device, etc.
|
||||||
|
|
||||||
|
Usually, it is enough to call this method once per new device, as its effects
|
||||||
|
will persist across reboots. However, it is safe to call this method multiple
|
||||||
|
times. It may therefore be a good practice to always call it once at the
|
||||||
|
beginning of a script to ensure that subsequent interactions will succeed.
|
||||||
|
|
||||||
|
Optionally, this may also be used to deploy additional tools to the device
|
||||||
|
by specifying a list of binaries to install in the ``executables`` parameter.
|
||||||
|
|
||||||
|
.. method:: Target.reboot([hard [, connect, [timeout]]])
|
||||||
|
|
||||||
|
Reboot the target device.
|
||||||
|
|
||||||
|
:param hard: A boolean value. If ``True`` a hard reset will be used instead
|
||||||
|
of the usual soft reset. Hard reset must be supported (usually via a
|
||||||
|
module) for this to work. Defaults to ``False``.
|
||||||
|
:param connect: A boolean value. If ``True``, a connection will be
|
||||||
|
automatically established to the target after reboot. Defaults to
|
||||||
|
``True``.
|
||||||
|
:param timeout: If set, this will be used by various (platform-specific)
|
||||||
|
operations during reboot process to detect if the reboot has failed and
|
||||||
|
the device has hung.
|
||||||
|
|
||||||
|
.. method:: Target.push(source, dest [, timeout])
|
||||||
|
|
||||||
|
Transfer a file from the host machine to the target device.
|
||||||
|
|
||||||
|
:param source: path of to the file on the host
|
||||||
|
:param dest: path of to the file on the target
|
||||||
|
:param timeout: timeout (in seconds) for the transfer; if the transfer does
|
||||||
|
not complete within this period, an exception will be raised.
|
||||||
|
|
||||||
|
.. method:: Target.pull(source, dest [, timeout])
|
||||||
|
|
||||||
|
Transfer a file from the target device to the host machine.
|
||||||
|
|
||||||
|
:param source: path of to the file on the target
|
||||||
|
:param dest: path of to the file on the host
|
||||||
|
:param timeout: timeout (in seconds) for the transfer; if the transfer does
|
||||||
|
not complete within this period, an exception will be raised.
|
||||||
|
|
||||||
|
.. method:: Target.execute(command [, timeout [, check_exit_code [, as_root]]])
|
||||||
|
|
||||||
|
Execute the specified command on the target device and return its output.
|
||||||
|
|
||||||
|
:param command: The command to be executed.
|
||||||
|
:param timeout: Timeout (in seconds) for the execution of the command. If
|
||||||
|
specified, an exception will be raised if execution does not complete
|
||||||
|
with the specified period.
|
||||||
|
:param check_exit_code: If ``True`` (the default) the exit code (on target)
|
||||||
|
from execution of the command will be checked, and an exception will be
|
||||||
|
raised if it is not ``0``.
|
||||||
|
:param as_root: The command will be executed as root. This will fail on
|
||||||
|
unrooted targets.
|
||||||
|
|
||||||
|
.. method:: Target.background(command [, stdout [, stderr [, as_root]]])
|
||||||
|
|
||||||
|
Execute the command on the target, invoking it via subprocess on the host.
|
||||||
|
This will return :class:`subprocess.Popen` instance for the command.
|
||||||
|
|
||||||
|
:param command: The command to be executed.
|
||||||
|
:param stdout: By default, standard output will be piped from the subprocess;
|
||||||
|
this may be used to redirect it to an alternative file handle.
|
||||||
|
:param stderr: By default, standard error will be piped from the subprocess;
|
||||||
|
this may be used to redirect it to an alternative file handle.
|
||||||
|
:param as_root: The command will be executed as root. This will fail on
|
||||||
|
unrooted targets.
|
||||||
|
|
||||||
|
.. note:: This **will block the connection** until the command completes.
|
||||||
|
|
||||||
|
.. method:: Target.invoke(binary [, args [, in_directory [, on_cpus [, as_root [, timeout]]]]])
|
||||||
|
|
||||||
|
Execute the specified binary on target (must already be installed) under the
|
||||||
|
specified conditions and return the output.
|
||||||
|
|
||||||
|
:param binary: binary to execute. Must be present and executable on the device.
|
||||||
|
:param args: arguments to be passed to the binary. The can be either a list or
|
||||||
|
a string.
|
||||||
|
:param in_directory: execute the binary in the specified directory. This must
|
||||||
|
be an absolute path.
|
||||||
|
:param on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
|
||||||
|
case, it will be interpreted as the mask), a list of ``ints``, in which
|
||||||
|
case this will be interpreted as the list of cpus, or string, which
|
||||||
|
will be interpreted as a comma-separated list of cpu ranges, e.g.
|
||||||
|
``"0,4-7"``.
|
||||||
|
:param as_root: Specify whether the command should be run as root
|
||||||
|
:param timeout: If this is specified and invocation does not terminate within this number
|
||||||
|
of seconds, an exception will be raised.
|
||||||
|
|
||||||
|
.. method:: Target.kick_off(command [, as_root])
|
||||||
|
|
||||||
|
Kick off the specified command on the target and return immediately. Unlike
|
||||||
|
``background()`` this will not block the connection; on the other hand, there
|
||||||
|
is not way to know when the command finishes (apart from calling ``ps()``)
|
||||||
|
or to get its output (unless its redirected into a file that can be pulled
|
||||||
|
later as part of the command).
|
||||||
|
|
||||||
|
:param command: The command to be executed.
|
||||||
|
:param as_root: The command will be executed as root. This will fail on
|
||||||
|
unrooted targets.
|
||||||
|
|
||||||
|
.. method:: Target.read_value(path [,kind])
|
||||||
|
|
||||||
|
Read the value from the specified path. This is primarily intended for
|
||||||
|
sysfs/procfs/debugfs etc.
|
||||||
|
|
||||||
|
:param path: file to read
|
||||||
|
:param kind: Optionally, read value will be converted into the specified
|
||||||
|
kind (which should be a callable that takes exactly one parameter).
|
||||||
|
|
||||||
|
.. method:: Target.read_int(self, path)
|
||||||
|
|
||||||
|
Equivalent to ``Target.read_value(path, kind=devlab.utils.types.integer)``
|
||||||
|
|
||||||
|
.. method:: Target.read_bool(self, path)
|
||||||
|
|
||||||
|
Equivalent to ``Target.read_value(path, kind=devlab.utils.types.boolean)``
|
||||||
|
|
||||||
|
.. method:: Target.write_value(path, value [, verify])
|
||||||
|
|
||||||
|
Write the value to the specified path on the target. This is primarily
|
||||||
|
intended for sysfs/procfs/debugfs etc.
|
||||||
|
|
||||||
|
:param path: file to write into
|
||||||
|
:param value: value to be written
|
||||||
|
:param verify: If ``True`` (the default) the value will be read back after
|
||||||
|
it is written to make sure it has been written successfully. This due to
|
||||||
|
some sysfs entries silently failing to set the written value without
|
||||||
|
returning an error code.
|
||||||
|
|
||||||
|
.. method:: Target.reset()
|
||||||
|
|
||||||
|
Soft reset the target. Typically, this means executing ``reboot`` on the
|
||||||
|
target.
|
||||||
|
|
||||||
|
.. method:: Target.check_responsive()
|
||||||
|
|
||||||
|
Returns ``True`` if the target appears to be responsive and ``False``
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
.. method:: Target.kill(pid[, signal[, as_root]])
|
||||||
|
|
||||||
|
Kill a process on the target.
|
||||||
|
|
||||||
|
:param pid: PID of the process to be killed.
|
||||||
|
:param signal: Signal to be used to kill the process. Defaults to
|
||||||
|
``signal.SIGTERM``.
|
||||||
|
:param as_root: If set to ``True``, kill will be issued as root. This will
|
||||||
|
fail on unrooted targets.
|
||||||
|
|
||||||
|
.. method:: Target.killall(name[, signal[, as_root]])
|
||||||
|
|
||||||
|
Kill all processes with the specified name on the target. Other parameters
|
||||||
|
are the same as for ``kill()``.
|
||||||
|
|
||||||
|
.. method:: Target.get_pids_of(name)
|
||||||
|
|
||||||
|
Return a list of PIDs of all running instances of the specified process.
|
||||||
|
|
||||||
|
.. method:: Target.ps()
|
||||||
|
|
||||||
|
Return a list of :class:`PsEntry` instances for all running processes on the
|
||||||
|
system.
|
||||||
|
|
||||||
|
.. method:: Target.file_exists(self, filepath)
|
||||||
|
|
||||||
|
Returns ``True`` if the specified path exists on the target and ``False``
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
.. method:: Target.list_file_systems()
|
||||||
|
|
||||||
|
Lists file systems mounted on the target. Returns a list of
|
||||||
|
:class:`FstabEntry`\ s.
|
||||||
|
|
||||||
|
.. method:: Target.list_directory(path[, as_root])
|
||||||
|
|
||||||
|
List (optionally, as root) the contents of the specified directory. Returns a
|
||||||
|
list of strings.
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: Target.get_workpath(self, path)
|
||||||
|
|
||||||
|
Convert the specified path to an absolute path relative to
|
||||||
|
``working_directory`` on the target. This is a shortcut for
|
||||||
|
``t.path.join(t.working_directory, path)``
|
||||||
|
|
||||||
|
.. method:: Target.tempfile([prefix [, suffix]])
|
||||||
|
|
||||||
|
Get a path to a temporary file (optionally, with the specified prefix and/or
|
||||||
|
suffix) on the target.
|
||||||
|
|
||||||
|
.. method:: Target.remove(path[, as_root])
|
||||||
|
|
||||||
|
Delete the specified path on the target. Will work on files and directories.
|
||||||
|
|
||||||
|
.. method:: Target.core_cpus(core)
|
||||||
|
|
||||||
|
Return a list of numeric cpu IDs corresponding to the specified core name.
|
||||||
|
|
||||||
|
.. method:: Target.list_online_cpus([core])
|
||||||
|
|
||||||
|
Return a list of numeric cpu IDs for all online CPUs (optionally, only for
|
||||||
|
CPUs corresponding to the specified core).
|
||||||
|
|
||||||
|
.. method:: Target.list_offline_cpus([core])
|
||||||
|
|
||||||
|
Return a list of numeric cpu IDs for all offline CPUs (optionally, only for
|
||||||
|
CPUs corresponding to the specified core).
|
||||||
|
|
||||||
|
.. method:: Target.getenv(variable)
|
||||||
|
|
||||||
|
Return the value of the specified environment variable on the device
|
||||||
|
|
||||||
|
.. method:: Target.capture_screen(filepath)
|
||||||
|
|
||||||
|
Take a screenshot on the device and save it to the specified file on the
|
||||||
|
host. This may not be supported by the target.
|
||||||
|
|
||||||
|
.. method:: Target.install(filepath[, timeout[, with_name]])
|
||||||
|
|
||||||
|
Install an executable on the device.
|
||||||
|
|
||||||
|
:param filepath: path to the executable on the host
|
||||||
|
:param timeout: Optional timeout (in seconds) for the installation
|
||||||
|
:param with_name: This may be used to rename the executable on the target
|
||||||
|
|
||||||
|
.. method:: Target.uninstall(name)
|
||||||
|
|
||||||
|
Uninstall the specified executable from the target
|
||||||
|
|
||||||
|
.. method:: Target.get_installed(name)
|
||||||
|
|
||||||
|
Return the full installation path on the target for the specified executable,
|
||||||
|
or ``None`` if the executable is not installed.
|
||||||
|
|
||||||
|
.. method:: Target.which(name)
|
||||||
|
|
||||||
|
Alias for ``get_installed()``
|
||||||
|
|
||||||
|
.. method:: Target.is_installed(name)
|
||||||
|
|
||||||
|
Returns ``True`` if an executable with the specified name is installed on the
|
||||||
|
target and ``False`` other wise.
|
||||||
|
|
89
setup.py
Normal file
89
setup.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# Copyright 2013-2015 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
try:
|
||||||
|
from setuptools import setup
|
||||||
|
except ImportError:
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
|
||||||
|
devlib_dir = os.path.join(os.path.dirname(__file__), 'devlib')
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(devlib_dir, 'core'))
|
||||||
|
|
||||||
|
# happends if falling back to distutils
|
||||||
|
warnings.filterwarnings('ignore', "Unknown distribution option: 'install_requires'")
|
||||||
|
warnings.filterwarnings('ignore', "Unknown distribution option: 'extras_require'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove('MANIFEST')
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
packages = []
|
||||||
|
data_files = {}
|
||||||
|
source_dir = os.path.dirname(__file__)
|
||||||
|
for root, dirs, files in os.walk(devlib_dir):
|
||||||
|
rel_dir = os.path.relpath(root, source_dir)
|
||||||
|
data = []
|
||||||
|
if '__init__.py' in files:
|
||||||
|
for f in files:
|
||||||
|
if os.path.splitext(f)[1] not in ['.py', '.pyc', '.pyo']:
|
||||||
|
data.append(f)
|
||||||
|
package_name = rel_dir.replace(os.sep, '.')
|
||||||
|
package_dir = root
|
||||||
|
packages.append(package_name)
|
||||||
|
data_files[package_name] = data
|
||||||
|
else:
|
||||||
|
# use previous package name
|
||||||
|
filepaths = [os.path.join(root, f) for f in files]
|
||||||
|
data_files[package_name].extend([os.path.relpath(f, package_dir) for f in filepaths])
|
||||||
|
|
||||||
|
params = dict(
|
||||||
|
name='devlib',
|
||||||
|
description='A framework for automating workload execution and measurment collection on ARM devices.',
|
||||||
|
version='0.0.1',
|
||||||
|
packages=packages,
|
||||||
|
package_data=data_files,
|
||||||
|
url='N/A',
|
||||||
|
license='Apache v2',
|
||||||
|
maintainer='ARM Ltd.',
|
||||||
|
install_requires=[
|
||||||
|
'python-dateutil', # converting between UTC and local time.
|
||||||
|
'pexpect>=3.3', # Send/recieve to/from device
|
||||||
|
'pyserial', # Serial port interface
|
||||||
|
],
|
||||||
|
extras_require={
|
||||||
|
'daq': ['daqpower'],
|
||||||
|
'doc': ['sphinx'],
|
||||||
|
},
|
||||||
|
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 4 - Beta',
|
||||||
|
'License :: OSI Approved :: Apache Software License',
|
||||||
|
'Operating System :: POSIX :: Linux',
|
||||||
|
'Programming Language :: Python :: 2.7',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
all_extras = list(chain(params['extras_require'].itervalues()))
|
||||||
|
params['extras_require']['full'] = all_extras
|
||||||
|
|
||||||
|
setup(**params)
|
14
src/netstats/AndroidManifest.xml
Normal file
14
src/netstats/AndroidManifest.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.arm.devlib.netstats"
|
||||||
|
android:versionCode="1"
|
||||||
|
android:versionName="1.0">
|
||||||
|
<application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
|
||||||
|
<service android:name="com.arm.devlib.netstats.TrafficMetricsService" android:exported="true" android:enabled="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.arm.devlib.netstats.TrafficMetricsService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
|
</manifest>
|
8
src/netstats/build.sh
Executable file
8
src/netstats/build.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
set -e
|
||||||
|
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||||
|
pushd $THIS_DIR
|
||||||
|
ant release
|
||||||
|
jarsigner -verbose -keystore ~/.android/debug.keystore -storepass android -keypass android $THIS_DIR/bin/netstats-*.apk androiddebugkey
|
||||||
|
cp $THIS_DIR/bin/netstats-*.apk $THIS_DIR/../../devlib/instrument/netstats/netstats.apk
|
||||||
|
ant clean
|
||||||
|
popd
|
92
src/netstats/build.xml
Normal file
92
src/netstats/build.xml
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project name="netstats" default="help">
|
||||||
|
|
||||||
|
<!-- The local.properties file is created and updated by the 'android' tool.
|
||||||
|
It contains the path to the SDK. It should *NOT* be checked into
|
||||||
|
Version Control Systems. -->
|
||||||
|
<property file="local.properties" />
|
||||||
|
|
||||||
|
<!-- The ant.properties file can be created by you. It is only edited by the
|
||||||
|
'android' tool to add properties to it.
|
||||||
|
This is the place to change some Ant specific build properties.
|
||||||
|
Here are some properties you may want to change/update:
|
||||||
|
|
||||||
|
source.dir
|
||||||
|
The name of the source directory. Default is 'src'.
|
||||||
|
out.dir
|
||||||
|
The name of the output directory. Default is 'bin'.
|
||||||
|
|
||||||
|
For other overridable properties, look at the beginning of the rules
|
||||||
|
files in the SDK, at tools/ant/build.xml
|
||||||
|
|
||||||
|
Properties related to the SDK location or the project target should
|
||||||
|
be updated using the 'android' tool with the 'update' action.
|
||||||
|
|
||||||
|
This file is an integral part of the build system for your
|
||||||
|
application and should be checked into Version Control Systems.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<property file="ant.properties" />
|
||||||
|
|
||||||
|
<!-- if sdk.dir was not set from one of the property file, then
|
||||||
|
get it from the ANDROID_HOME env var.
|
||||||
|
This must be done before we load project.properties since
|
||||||
|
the proguard config can use sdk.dir -->
|
||||||
|
<property environment="env" />
|
||||||
|
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
|
||||||
|
<isset property="env.ANDROID_HOME" />
|
||||||
|
</condition>
|
||||||
|
|
||||||
|
<!-- The project.properties file is created and updated by the 'android'
|
||||||
|
tool, as well as ADT.
|
||||||
|
|
||||||
|
This contains project specific properties such as project target, and library
|
||||||
|
dependencies. Lower level build properties are stored in ant.properties
|
||||||
|
(or in .classpath for Eclipse projects).
|
||||||
|
|
||||||
|
This file is an integral part of the build system for your
|
||||||
|
application and should be checked into Version Control Systems. -->
|
||||||
|
<loadproperties srcFile="project.properties" />
|
||||||
|
|
||||||
|
<!-- quick check on sdk.dir -->
|
||||||
|
<fail
|
||||||
|
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
|
||||||
|
unless="sdk.dir"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Import per project custom build rules if present at the root of the project.
|
||||||
|
This is the place to put custom intermediary targets such as:
|
||||||
|
-pre-build
|
||||||
|
-pre-compile
|
||||||
|
-post-compile (This is typically used for code obfuscation.
|
||||||
|
Compiled code location: ${out.classes.absolute.dir}
|
||||||
|
If this is not done in place, override ${out.dex.input.absolute.dir})
|
||||||
|
-post-package
|
||||||
|
-post-build
|
||||||
|
-pre-clean
|
||||||
|
-->
|
||||||
|
<import file="custom_rules.xml" optional="true" />
|
||||||
|
|
||||||
|
<!-- Import the actual build file.
|
||||||
|
|
||||||
|
To customize existing targets, there are two options:
|
||||||
|
- Customize only one target:
|
||||||
|
- copy/paste the target into this file, *before* the
|
||||||
|
<import> task.
|
||||||
|
- customize it to your needs.
|
||||||
|
- Customize the whole content of build.xml
|
||||||
|
- copy/paste the content of the rules files (minus the top node)
|
||||||
|
into this file, replacing the <import> task.
|
||||||
|
- customize to your needs.
|
||||||
|
|
||||||
|
***********************
|
||||||
|
****** IMPORTANT ******
|
||||||
|
***********************
|
||||||
|
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
|
||||||
|
in order to avoid having your file be overridden by tools such as "android update project"
|
||||||
|
-->
|
||||||
|
<!-- version-tag: 1 -->
|
||||||
|
<import file="${sdk.dir}/tools/ant/build.xml" />
|
||||||
|
|
||||||
|
</project>
|
20
src/netstats/proguard-project.txt
Normal file
20
src/netstats/proguard-project.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# To enable ProGuard in your project, edit project.properties
|
||||||
|
# to define the proguard.config property as described in that file.
|
||||||
|
#
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in ${sdk.dir}/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the ProGuard
|
||||||
|
# include property in project.properties.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
14
src/netstats/project.properties
Normal file
14
src/netstats/project.properties
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# This file is automatically generated by Android Tools.
|
||||||
|
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
|
||||||
|
#
|
||||||
|
# This file must be checked in Version Control Systems.
|
||||||
|
#
|
||||||
|
# To customize properties used by the Ant build system edit
|
||||||
|
# "ant.properties", and override values to adapt the script to your
|
||||||
|
# project structure.
|
||||||
|
#
|
||||||
|
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
|
||||||
|
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
|
||||||
|
|
||||||
|
# Project target.
|
||||||
|
target=Google Inc.:Google APIs (x86 System Image):19
|
BIN
src/netstats/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
src/netstats/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
BIN
src/netstats/res/drawable-ldpi/ic_launcher.png
Normal file
BIN
src/netstats/res/drawable-ldpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
src/netstats/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
src/netstats/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
src/netstats/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
src/netstats/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
13
src/netstats/res/layout/main.xml
Normal file
13
src/netstats/res/layout/main.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
>
|
||||||
|
<TextView
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Hello World, NetStats"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
4
src/netstats/res/values/strings.xml
Normal file
4
src/netstats/res/values/strings.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">NetStats</string>
|
||||||
|
</resources>
|
@ -0,0 +1,124 @@
|
|||||||
|
package com.arm.devlib.netstats;
|
||||||
|
|
||||||
|
import java.lang.InterruptedException;
|
||||||
|
import java.lang.System;
|
||||||
|
import java.lang.Thread;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.TrafficStats;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
class TrafficPoller implements Runnable {
|
||||||
|
|
||||||
|
private String tag;
|
||||||
|
private int period;
|
||||||
|
private PackageManager pm;
|
||||||
|
private static String TAG = "TrafficMetrics";
|
||||||
|
private List<String> packageNames;
|
||||||
|
private Map<String, Map<String, Long>> previousValues;
|
||||||
|
|
||||||
|
public TrafficPoller(String tag, PackageManager pm, int period, List<String> packages) {
|
||||||
|
this.tag = tag;
|
||||||
|
this.pm = pm;
|
||||||
|
this.period = period;
|
||||||
|
this.packageNames = packages;
|
||||||
|
this.previousValues = new HashMap<String, Map<String, Long>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
Thread.sleep(this.period);
|
||||||
|
getPakagesInfo();
|
||||||
|
if (Thread.interrupted()) {
|
||||||
|
throw new InterruptedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getPakagesInfo() {
|
||||||
|
List<ApplicationInfo> apps;
|
||||||
|
if (this.packageNames == null) {
|
||||||
|
apps = pm.getInstalledApplications(0);
|
||||||
|
for (ApplicationInfo app : apps) {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apps = new ArrayList<ApplicationInfo>();
|
||||||
|
for (String packageName : packageNames) {
|
||||||
|
try {
|
||||||
|
ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
|
||||||
|
apps.add(info);
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ApplicationInfo appInfo : apps) {
|
||||||
|
int uid = appInfo.uid;
|
||||||
|
String name = appInfo.packageName;
|
||||||
|
long time = System.currentTimeMillis();
|
||||||
|
long received = TrafficStats.getUidRxBytes(uid);
|
||||||
|
long sent = TrafficStats.getUidTxBytes(uid);
|
||||||
|
|
||||||
|
if (!this.previousValues.containsKey(name)) {
|
||||||
|
this.previousValues.put(name, new HashMap<String, Long>());
|
||||||
|
this.previousValues.get(name).put("sent", sent);
|
||||||
|
this.previousValues.get(name).put("received", received);
|
||||||
|
Log.i(this.tag, String.format("INITIAL \"%s\" TX: %d RX: %d",
|
||||||
|
name, sent, received));
|
||||||
|
} else {
|
||||||
|
long previosSent = this.previousValues.get(name).put("sent", sent);
|
||||||
|
long previosReceived = this.previousValues.get(name).put("received", received);
|
||||||
|
Log.i(this.tag, String.format("%d \"%s\" TX: %d RX: %d",
|
||||||
|
time, name,
|
||||||
|
sent - previosSent,
|
||||||
|
received - previosReceived));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TrafficMetricsService extends IntentService {
|
||||||
|
|
||||||
|
private static String TAG = "TrafficMetrics";
|
||||||
|
private Thread thread;
|
||||||
|
private static int defaultPollingPeriod = 5000;
|
||||||
|
|
||||||
|
public TrafficMetricsService() {
|
||||||
|
super("TrafficMetrics");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHandleIntent(Intent intent) {
|
||||||
|
List<String> packages = null;
|
||||||
|
String runTag = intent.getStringExtra("tag");
|
||||||
|
if (runTag == null) {
|
||||||
|
runTag = TAG;
|
||||||
|
}
|
||||||
|
String packagesString = intent.getStringExtra("packages");
|
||||||
|
int pollingPeriod = intent.getIntExtra("period", this.defaultPollingPeriod);
|
||||||
|
if (packagesString != null) {
|
||||||
|
packages = new ArrayList<String>(Arrays.asList(packagesString.split(",")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.thread != null) {
|
||||||
|
Log.e(runTag, "Attemping to start when monitoring is already in progress.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.thread = new Thread(new TrafficPoller(runTag, getPackageManager(), pollingPeriod, packages));
|
||||||
|
this.thread.start();
|
||||||
|
}
|
||||||
|
}
|
11
src/readenergy/Makefile
Normal file
11
src/readenergy/Makefile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# To build:
|
||||||
|
#
|
||||||
|
# CROSS_COMPILE=aarch64-linux-gnu- make
|
||||||
|
#
|
||||||
|
CROSS_COMPILE?=aarch64-linux-gnu-
|
||||||
|
CC=$(CROSS_COMPILE)gcc
|
||||||
|
CFLAGS='-Wl,-static -Wl,-lc'
|
||||||
|
|
||||||
|
readenergy: readenergy.c
|
||||||
|
$(CC) $(CFLAGS) readenergy.c -o readenergy
|
||||||
|
mv readenergy ../../devlib/bin/arm64/readenergy
|
345
src/readenergy/readenergy.c
Normal file
345
src/readenergy/readenergy.c
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
/* Copyright 2014-2015 ARM Limited
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* readenergy.c
|
||||||
|
*
|
||||||
|
* Reads APB energy registers in Juno and outputs the measurements (converted to appropriate units).
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
// The following values obtained from Juno TRM 2014/03/04 section 4.5
|
||||||
|
|
||||||
|
// Location of APB registers in memory
|
||||||
|
#define APB_BASE_MEMORY 0x1C010000
|
||||||
|
// APB energy counters start at offset 0xD0 from the base APB address.
|
||||||
|
#define BASE_INDEX 0xD0 / 4
|
||||||
|
// the one-past last APB counter
|
||||||
|
#define APB_SIZE 0x120
|
||||||
|
|
||||||
|
// Masks specifying the bits that contain the actual counter values
|
||||||
|
#define CMASK 0xFFF
|
||||||
|
#define VMASK 0xFFF
|
||||||
|
#define PMASK 0xFFFFFF
|
||||||
|
|
||||||
|
// Sclaing factor (divisor) or getting measured values from counters
|
||||||
|
#define SYS_ADC_CH0_PM1_SYS_SCALE 761
|
||||||
|
#define SYS_ADC_CH1_PM2_A57_SCALE 381
|
||||||
|
#define SYS_ADC_CH2_PM3_A53_SCALE 761
|
||||||
|
#define SYS_ADC_CH3_PM4_GPU_SCALE 381
|
||||||
|
#define SYS_ADC_CH4_VSYS_SCALE 1622
|
||||||
|
#define SYS_ADC_CH5_VA57_SCALE 1622
|
||||||
|
#define SYS_ADC_CH6_VA53_SCALE 1622
|
||||||
|
#define SYS_ADC_CH7_VGPU_SCALE 1622
|
||||||
|
#define SYS_POW_CH04_SYS_SCALE (SYS_ADC_CH0_PM1_SYS_SCALE * SYS_ADC_CH4_VSYS_SCALE)
|
||||||
|
#define SYS_POW_CH15_A57_SCALE (SYS_ADC_CH1_PM2_A57_SCALE * SYS_ADC_CH5_VA57_SCALE)
|
||||||
|
#define SYS_POW_CH26_A53_SCALE (SYS_ADC_CH2_PM3_A53_SCALE * SYS_ADC_CH6_VA53_SCALE)
|
||||||
|
#define SYS_POW_CH37_GPU_SCALE (SYS_ADC_CH3_PM4_GPU_SCALE * SYS_ADC_CH7_VGPU_SCALE)
|
||||||
|
#define SYS_ENM_CH0_SYS_SCALE 12348030000
|
||||||
|
#define SYS_ENM_CH1_A57_SCALE 6174020000
|
||||||
|
#define SYS_ENM_CH0_A53_SCALE 12348030000
|
||||||
|
#define SYS_ENM_CH0_GPU_SCALE 6174020000
|
||||||
|
|
||||||
|
// Original values prior to re-callibrations.
|
||||||
|
/*#define SYS_ADC_CH0_PM1_SYS_SCALE 819.2*/
|
||||||
|
/*#define SYS_ADC_CH1_PM2_A57_SCALE 409.6*/
|
||||||
|
/*#define SYS_ADC_CH2_PM3_A53_SCALE 819.2*/
|
||||||
|
/*#define SYS_ADC_CH3_PM4_GPU_SCALE 409.6*/
|
||||||
|
/*#define SYS_ADC_CH4_VSYS_SCALE 1638.4*/
|
||||||
|
/*#define SYS_ADC_CH5_VA57_SCALE 1638.4*/
|
||||||
|
/*#define SYS_ADC_CH6_VA53_SCALE 1638.4*/
|
||||||
|
/*#define SYS_ADC_CH7_VGPU_SCALE 1638.4*/
|
||||||
|
/*#define SYS_POW_CH04_SYS_SCALE (SYS_ADC_CH0_PM1_SYS_SCALE * SYS_ADC_CH4_VSYS_SCALE)*/
|
||||||
|
/*#define SYS_POW_CH15_A57_SCALE (SYS_ADC_CH1_PM2_A57_SCALE * SYS_ADC_CH5_VA57_SCALE)*/
|
||||||
|
/*#define SYS_POW_CH26_A53_SCALE (SYS_ADC_CH2_PM3_A53_SCALE * SYS_ADC_CH6_VA53_SCALE)*/
|
||||||
|
/*#define SYS_POW_CH37_GPU_SCALE (SYS_ADC_CH3_PM4_GPU_SCALE * SYS_ADC_CH7_VGPU_SCALE)*/
|
||||||
|
/*#define SYS_ENM_CH0_SYS_SCALE 13421772800.0*/
|
||||||
|
/*#define SYS_ENM_CH1_A57_SCALE 6710886400.0*/
|
||||||
|
/*#define SYS_ENM_CH0_A53_SCALE 13421772800.0*/
|
||||||
|
/*#define SYS_ENM_CH0_GPU_SCALE 6710886400.0*/
|
||||||
|
|
||||||
|
// Ignore individual errors but if see too many, abort.
|
||||||
|
#define ERROR_THRESHOLD 10
|
||||||
|
|
||||||
|
// Default counter poll period (in milliseconds).
|
||||||
|
#define DEFAULT_PERIOD 100
|
||||||
|
|
||||||
|
// A single reading from the energy meter. The values are the proper readings converted
|
||||||
|
// to appropriate units (e.g. Watts for power); they are *not* raw counter values.
|
||||||
|
struct reading
|
||||||
|
{
|
||||||
|
double sys_adc_ch0_pm1_sys;
|
||||||
|
double sys_adc_ch1_pm2_a57;
|
||||||
|
double sys_adc_ch2_pm3_a53;
|
||||||
|
double sys_adc_ch3_pm4_gpu;
|
||||||
|
double sys_adc_ch4_vsys;
|
||||||
|
double sys_adc_ch5_va57;
|
||||||
|
double sys_adc_ch6_va53;
|
||||||
|
double sys_adc_ch7_vgpu;
|
||||||
|
double sys_pow_ch04_sys;
|
||||||
|
double sys_pow_ch15_a57;
|
||||||
|
double sys_pow_ch26_a53;
|
||||||
|
double sys_pow_ch37_gpu;
|
||||||
|
double sys_enm_ch0_sys;
|
||||||
|
double sys_enm_ch1_a57;
|
||||||
|
double sys_enm_ch0_a53;
|
||||||
|
double sys_enm_ch0_gpu;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline uint64_t join_64bit_register(uint32_t *buffer, int index)
|
||||||
|
{
|
||||||
|
uint64_t result = 0;
|
||||||
|
result |= buffer[index];
|
||||||
|
result |= (uint64_t)(buffer[index+1]) << 32;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nsleep(const struct timespec *req, struct timespec *rem)
|
||||||
|
{
|
||||||
|
struct timespec temp_rem;
|
||||||
|
if (nanosleep(req, rem) == -1)
|
||||||
|
{
|
||||||
|
if (errno == EINTR)
|
||||||
|
{
|
||||||
|
nsleep(rem, &temp_rem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return errno;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_help()
|
||||||
|
{
|
||||||
|
fprintf(stderr, "Usage: readenergy [-t PERIOD] -o OUTFILE\n\n"
|
||||||
|
"Read Juno energy counters every PERIOD milliseconds, writing them\n"
|
||||||
|
"to OUTFILE in CSV format until SIGTERM is received.\n\n"
|
||||||
|
"Parameters:\n"
|
||||||
|
" PERIOD is the counter poll period in milliseconds.\n"
|
||||||
|
" (Defaults to 100 milliseconds.)\n"
|
||||||
|
" OUTFILE is the output file path\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// debugging only...
|
||||||
|
inline void dprint(char *msg)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", msg);
|
||||||
|
sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------- config ----------------------------------------------------
|
||||||
|
|
||||||
|
struct config
|
||||||
|
{
|
||||||
|
struct timespec period;
|
||||||
|
char *output_file;
|
||||||
|
};
|
||||||
|
|
||||||
|
void config_init_period_from_millis(struct config *this, long millis)
|
||||||
|
{
|
||||||
|
this->period.tv_sec = (time_t)(millis / 1000);
|
||||||
|
this->period.tv_nsec = (millis % 1000) * 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
void config_init(struct config *this, int argc, char *argv[])
|
||||||
|
{
|
||||||
|
this->output_file = NULL;
|
||||||
|
config_init_period_from_millis(this, DEFAULT_PERIOD);
|
||||||
|
|
||||||
|
int opt;
|
||||||
|
while ((opt = getopt(argc, argv, "ht:o:")) != -1)
|
||||||
|
{
|
||||||
|
switch(opt)
|
||||||
|
{
|
||||||
|
case 't':
|
||||||
|
config_init_period_from_millis(this, atol(optarg));
|
||||||
|
break;
|
||||||
|
case 'o':
|
||||||
|
this->output_file = optarg;
|
||||||
|
break;
|
||||||
|
case 'h':
|
||||||
|
print_help();
|
||||||
|
exit(EXIT_SUCCESS);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fprintf(stderr, "ERROR: Unexpected option %s\n\n", opt);
|
||||||
|
print_help();
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->output_file == NULL)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "ERROR: Mandatory -o option not specified.\n\n");
|
||||||
|
print_help();
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------- /config ---------------------------------------------------
|
||||||
|
|
||||||
|
// -------------------------------------- emeter ----------------------------------------------------
|
||||||
|
|
||||||
|
struct emeter
|
||||||
|
{
|
||||||
|
int fd;
|
||||||
|
FILE *out;
|
||||||
|
void *mmap_base;
|
||||||
|
};
|
||||||
|
|
||||||
|
void emeter_init(struct emeter *this, char *outfile)
|
||||||
|
{
|
||||||
|
this->out = fopen(outfile, "w");
|
||||||
|
if (this->out == NULL)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "ERROR: Could not open output file %s; got %s\n", outfile, strerror(errno));
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->fd = open("/dev/mem", O_RDONLY);
|
||||||
|
if(this->fd < 0)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "ERROR: Can't open /dev/mem; got %s\n", strerror(errno));
|
||||||
|
fclose(this->out);
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->mmap_base = mmap(NULL, APB_SIZE, PROT_READ, MAP_SHARED, this->fd, APB_BASE_MEMORY);
|
||||||
|
if (this->mmap_base == MAP_FAILED)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "ERROR: mmap failed; got %s\n", strerror(errno));
|
||||||
|
close(this->fd);
|
||||||
|
fclose(this->out);
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(this->out, "sys_curr,a57_curr,a53_curr,gpu_curr,"
|
||||||
|
"sys_volt,a57_volt,a53_volt,gpu_volt,"
|
||||||
|
"sys_pow,a57_pow,a53_pow,gpu_pow,"
|
||||||
|
"sys_cenr,a57_cenr,a53_cenr,gpu_cenr\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void emeter_read_measurements(struct emeter *this, struct reading *reading)
|
||||||
|
{
|
||||||
|
uint32_t *buffer = (uint32_t *)this->mmap_base;
|
||||||
|
reading->sys_adc_ch0_pm1_sys = (double)(CMASK & buffer[BASE_INDEX+0]) / SYS_ADC_CH0_PM1_SYS_SCALE;
|
||||||
|
reading->sys_adc_ch1_pm2_a57 = (double)(CMASK & buffer[BASE_INDEX+1]) / SYS_ADC_CH1_PM2_A57_SCALE;
|
||||||
|
reading->sys_adc_ch2_pm3_a53 = (double)(CMASK & buffer[BASE_INDEX+2]) / SYS_ADC_CH2_PM3_A53_SCALE;
|
||||||
|
reading->sys_adc_ch3_pm4_gpu = (double)(CMASK & buffer[BASE_INDEX+3]) / SYS_ADC_CH3_PM4_GPU_SCALE;
|
||||||
|
reading->sys_adc_ch4_vsys = (double)(VMASK & buffer[BASE_INDEX+4]) / SYS_ADC_CH4_VSYS_SCALE;
|
||||||
|
reading->sys_adc_ch5_va57 = (double)(VMASK & buffer[BASE_INDEX+5]) / SYS_ADC_CH5_VA57_SCALE;
|
||||||
|
reading->sys_adc_ch6_va53 = (double)(VMASK & buffer[BASE_INDEX+6]) / SYS_ADC_CH6_VA53_SCALE;
|
||||||
|
reading->sys_adc_ch7_vgpu = (double)(VMASK & buffer[BASE_INDEX+7]) / SYS_ADC_CH7_VGPU_SCALE;
|
||||||
|
reading->sys_pow_ch04_sys = (double)(PMASK & buffer[BASE_INDEX+8]) / SYS_POW_CH04_SYS_SCALE;
|
||||||
|
reading->sys_pow_ch15_a57 = (double)(PMASK & buffer[BASE_INDEX+9]) / SYS_POW_CH15_A57_SCALE;
|
||||||
|
reading->sys_pow_ch26_a53 = (double)(PMASK & buffer[BASE_INDEX+10]) / SYS_POW_CH26_A53_SCALE;
|
||||||
|
reading->sys_pow_ch37_gpu = (double)(PMASK & buffer[BASE_INDEX+11]) / SYS_POW_CH37_GPU_SCALE;
|
||||||
|
reading->sys_enm_ch0_sys = (double)join_64bit_register(buffer, BASE_INDEX+12) / SYS_ENM_CH0_SYS_SCALE;
|
||||||
|
reading->sys_enm_ch1_a57 = (double)join_64bit_register(buffer, BASE_INDEX+14) / SYS_ENM_CH1_A57_SCALE;
|
||||||
|
reading->sys_enm_ch0_a53 = (double)join_64bit_register(buffer, BASE_INDEX+16) / SYS_ENM_CH0_A53_SCALE;
|
||||||
|
reading->sys_enm_ch0_gpu = (double)join_64bit_register(buffer, BASE_INDEX+18) / SYS_ENM_CH0_GPU_SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void emeter_take_reading(struct emeter *this)
|
||||||
|
{
|
||||||
|
static struct reading reading;
|
||||||
|
int error_count = 0;
|
||||||
|
emeter_read_measurements(this, &reading);
|
||||||
|
int ret = fprintf(this->out, "%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f\n",
|
||||||
|
reading.sys_adc_ch0_pm1_sys,
|
||||||
|
reading.sys_adc_ch1_pm2_a57,
|
||||||
|
reading.sys_adc_ch2_pm3_a53,
|
||||||
|
reading.sys_adc_ch3_pm4_gpu,
|
||||||
|
reading.sys_adc_ch4_vsys,
|
||||||
|
reading.sys_adc_ch5_va57,
|
||||||
|
reading.sys_adc_ch6_va53,
|
||||||
|
reading.sys_adc_ch7_vgpu,
|
||||||
|
reading.sys_pow_ch04_sys,
|
||||||
|
reading.sys_pow_ch15_a57,
|
||||||
|
reading.sys_pow_ch26_a53,
|
||||||
|
reading.sys_pow_ch37_gpu,
|
||||||
|
reading.sys_enm_ch0_sys,
|
||||||
|
reading.sys_enm_ch1_a57,
|
||||||
|
reading.sys_enm_ch0_a53,
|
||||||
|
reading.sys_enm_ch0_gpu);
|
||||||
|
if (ret < 0)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "ERROR: while writing a meter reading: %s\n", strerror(errno));
|
||||||
|
if (++error_count > ERROR_THRESHOLD)
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void emeter_finalize(struct emeter *this)
|
||||||
|
{
|
||||||
|
if (munmap(this->mmap_base, APB_SIZE) == -1)
|
||||||
|
{
|
||||||
|
// Report the error but don't bother doing anything else, as we're not gonna do
|
||||||
|
// anything with emeter after this point anyway.
|
||||||
|
fprintf(stderr, "ERROR: munmap failed; got %s\n", strerror(errno));
|
||||||
|
}
|
||||||
|
close(this->fd);
|
||||||
|
fclose(this->out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------- /emeter ----------------------------------------------------
|
||||||
|
|
||||||
|
int done = 0;
|
||||||
|
|
||||||
|
void term_handler(int signum)
|
||||||
|
{
|
||||||
|
done = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
struct sigaction action;
|
||||||
|
memset(&action, 0, sizeof(struct sigaction));
|
||||||
|
action.sa_handler = term_handler;
|
||||||
|
sigaction(SIGTERM, &action, NULL);
|
||||||
|
|
||||||
|
struct config config;
|
||||||
|
struct emeter emeter;
|
||||||
|
config_init(&config, argc, argv);
|
||||||
|
emeter_init(&emeter, config.output_file);
|
||||||
|
|
||||||
|
struct timespec remaining;
|
||||||
|
while (!done)
|
||||||
|
{
|
||||||
|
emeter_take_reading(&emeter);
|
||||||
|
nsleep(&config.period, &remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
emeter_finalize(&emeter);
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user