Showing preview only (2,969K chars total). Download the full file or copy to clipboard to get everything.
Repository: Anodynous/stenogotchi
Branch: main
Commit: 54da07d0d681
Files: 104
Total size: 2.8 MB
Directory structure:
gitextract_1w8uxzjn/
├── .github/
│ └── ISSUE_TEMPLATE/
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── BUILDNOTES.md
├── CHANGELOG.md
├── LICENSE
├── README.md
├── initial_setup.sh
├── plover_plugin/
│ ├── setup.cfg
│ ├── setup.py
│ └── stenogotchi_link/
│ ├── __init__.py
│ ├── clients.py
│ ├── com.github.stenogotchi.conf
│ ├── keymap.py
│ ├── stenogotchi_link.py
│ └── wpm.py
├── requirements.txt
├── stenogotchi/
│ ├── __init__.py
│ ├── _version.py
│ ├── agent.py
│ ├── automata.py
│ ├── defaults.toml
│ ├── fs/
│ │ └── __init__.py
│ ├── log.py
│ ├── plugins/
│ │ ├── __init__.py
│ │ ├── cmd.py
│ │ └── default/
│ │ ├── buttonshim.py
│ │ ├── dict_lookup.py
│ │ ├── evdevkb.py
│ │ ├── example.py
│ │ ├── led.py
│ │ ├── logtail.py
│ │ ├── memtemp.py
│ │ ├── plover_link.py
│ │ ├── plover_link_btserver_sdp_record.xml
│ │ └── upslite.py
│ ├── ui/
│ │ ├── __init__.py
│ │ ├── components.py
│ │ ├── display.py
│ │ ├── faces.py
│ │ ├── fonts.py
│ │ ├── hw/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── libs/
│ │ │ │ ├── __init__.py
│ │ │ │ └── waveshare/
│ │ │ │ ├── __init__.py
│ │ │ │ └── v2/
│ │ │ │ ├── __init__.py
│ │ │ │ └── epd2in13_V2.py
│ │ │ └── waveshare2.py
│ │ ├── state.py
│ │ ├── view.py
│ │ └── web/
│ │ ├── __init__.py
│ │ ├── handler.py
│ │ ├── server.py
│ │ ├── static/
│ │ │ ├── css/
│ │ │ │ ├── jquery.jqplot.css
│ │ │ │ └── style.css
│ │ │ └── js/
│ │ │ ├── jquery.jqplot.js
│ │ │ ├── jquery.mobile/
│ │ │ │ ├── jquery.mobile-1.4.5.css
│ │ │ │ ├── jquery.mobile-1.4.5.js
│ │ │ │ ├── jquery.mobile.external-png-1.4.5.css
│ │ │ │ ├── jquery.mobile.icons-1.4.5.css
│ │ │ │ ├── jquery.mobile.inline-png-1.4.5.css
│ │ │ │ ├── jquery.mobile.inline-svg-1.4.5.css
│ │ │ │ ├── jquery.mobile.structure-1.4.5.css
│ │ │ │ └── jquery.mobile.theme-1.4.5.css
│ │ │ ├── jquery.timeago.js
│ │ │ ├── plugins/
│ │ │ │ ├── jqplot.BezierCurveRenderer.js
│ │ │ │ ├── jqplot.barRenderer.js
│ │ │ │ ├── jqplot.blockRenderer.js
│ │ │ │ ├── jqplot.bubbleRenderer.js
│ │ │ │ ├── jqplot.canvasAxisLabelRenderer.js
│ │ │ │ ├── jqplot.canvasAxisTickRenderer.js
│ │ │ │ ├── jqplot.canvasOverlay.js
│ │ │ │ ├── jqplot.canvasTextRenderer.js
│ │ │ │ ├── jqplot.categoryAxisRenderer.js
│ │ │ │ ├── jqplot.ciParser.js
│ │ │ │ ├── jqplot.cursor.js
│ │ │ │ ├── jqplot.dateAxisRenderer.js
│ │ │ │ ├── jqplot.donutRenderer.js
│ │ │ │ ├── jqplot.dragable.js
│ │ │ │ ├── jqplot.enhancedLegendRenderer.js
│ │ │ │ ├── jqplot.enhancedPieLegendRenderer.js
│ │ │ │ ├── jqplot.funnelRenderer.js
│ │ │ │ ├── jqplot.highlighter.js
│ │ │ │ ├── jqplot.json2.js
│ │ │ │ ├── jqplot.logAxisRenderer.js
│ │ │ │ ├── jqplot.mekkoAxisRenderer.js
│ │ │ │ ├── jqplot.mekkoRenderer.js
│ │ │ │ ├── jqplot.meterGaugeRenderer.js
│ │ │ │ ├── jqplot.mobile.js
│ │ │ │ ├── jqplot.ohlcRenderer.js
│ │ │ │ ├── jqplot.pieRenderer.js
│ │ │ │ ├── jqplot.pointLabels.js
│ │ │ │ ├── jqplot.pyramidAxisRenderer.js
│ │ │ │ ├── jqplot.pyramidGridRenderer.js
│ │ │ │ ├── jqplot.pyramidRenderer.js
│ │ │ │ └── jqplot.trendline.js
│ │ │ └── viewportHeight.js
│ │ └── templates/
│ │ ├── base.html
│ │ ├── index.html
│ │ ├── plugins.html
│ │ └── status.html
│ ├── utils.py
│ └── voice.py
└── stenogotchi.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots/debug logs**
If applicable, add screenshots or debug logs to help explain your problem.
**Software/hardware configuration (please complete the following information):**
- Stenogotchi version:
- Plugin version:
- OS:
- Hardware:
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
================================================
FILE: BUILDNOTES.md
================================================
# Build Notes
Connect all the parts before soldering anything in place. They will not function without properly secured connections, but checking how they fit together can save you from time-consuming mistakes. You'll need a soldering iron, solder and preferably also cutting pliers, a metal file and some flux to complete the build. Below you find some tips on how to fit the parts together as neatly as possible.
## Build Process
The depicted device did not have pre-soldered header pins, allowing me to shorten the pins on the underside rather than the top. Leaving the ends connecting to the display unaltered. Depending on your model you may therefore need to make some changes to the below steps.
- Push the header pins through from the underside, leaving 4.5 mm of the pins exposed on the top (pre-soldered headers extend 9 mm above the board for comparison).
- Layer the buttonSHIM on the top directly against the RPI0w.
- Solder the headers on the top to the buttonSHIM using plenty of flux. Leaving as much of the pins clean as possible so they still fit into the display's socket.
- Cut and remove the black plastic separator from the headers on the underside, leaving only bare pins on this side of the board as well.
- Solder the pins to the underside of the board, ensuring a good electrical connection to the RPI0w.
- Cut and file down the pins on the underside to make room for the UPS-Lite. Shortening them enough so they don't poke or scratch the battery.
- Secure the UPS-Lite using the included hexagonal nuts. The screws are just barely long enough to accommodate the thickness of both the RPI0w and buttonSHIM.
- Carefully press down the eINK module on the male pin headers without applying excessive force to the display itself.
## GPIO Header
Whether your RPI0w comes with pre-soldered male header or not, I recommend removing the black spacer. This way you can position the buttonSHIM directly against the board on the top with only the female header of the eINK display module and the buttonSHIM itself adding to the overall height of the device.
## Waveshare Display:
- In order to fit the screen onto the device as snugly as possible, make sure to desolder or by other means detach the large white connector. The yellow kapton tape didn't end up being needed since the covered components don't make contact with the RPI0w.


## ButtonSHIM:
- Take care to orient the module correctly.
- Soldering it into place you want to aim for small but secure solder joints. Any extra solder on the pins will limit how close to the board you can fit the screen. An X-Acto knife or small file can help with potentially needed clean-up.

## UPS-Lite:
- Make sure the pins on the bottom don't extend too far before screwing the module onto the board. They should not be allowed to touch the battery pack. I ended up filing the pins down quite a bit to ensure sufficient clearance. Shape and length of the pins is important for the pogo pins to make good contact.
- In the picture you can also spot a couple of the pins on the top and just how low the solder joints need to be for connecting the display.

## Real Time Clock
- If you want to add a RTC module to keep the device synced while powered off, I recommend the [DS3231](https://www.pishop.us/product/ds3231-real-time-clock-module-for-raspberry-pi/). A good [setup guide](https://learn.adafruit.com/adding-a-real-time-clock-to-raspberry-pi/set-rtc-time) has been published by Adafruit.
- Removing the female headers, the module fits neatly inside the UPS-Lite. You can see the correct wiring and positioning in the below picture. Don't forget to isolate it with some tape and form neat solder joints for the pins shared by the UPS-Lite.

================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.0] - 2022-01-02
### Added
- Modifier, function and navigation-key support, both individually and in combination (unsupported inputs logged in plover.log).
- Reactions to new records in WPM-tracking mode.
- Plover dictionary lookup with eINK display and STENO input support. The plugin (dict_lookup) utilizes native Plover lookup functionality for full compatibility with supported dictionary formats.
- Buttonshim actions, both built-in and custom, accessible through web UI.
- Hardware build guide.
### Changed
- Plover v4.0.0.dev10 or higher required.
- Stenogotchi_link v0.3.0 required. Plugin now logs to plover.log.
- Character and personality adjustments.
- Simplified installation process.
- Added documentation for aligning text output with target device expected input language and layout.
- Web UI default port changed from 8080 to 80.
- Bluetooth connection established only once Plover has started.
### Fixed
- Significantly reduced input latency in STENO mode.
- Improved Bluetooth pairing and connection management.
- Web UI now starts only once a wifi connection has been established.
## [Unreleased]
### Added
### Changed
### Fixed
### Removed
## [0.0.5] - 2021-03-24
### Added
- DS3231 real time clock module wiring and positioning reference picture to README.
- Four new faces, producing processing animation when combined.
- WPM stats now track and display top result for session.
### Changed
- Led plugin and default patterns to better indicate noteworthy events.
### Fixed
- QWERTY-mode breaking bug introduced in v0.0.4.
- Letter capitalization, symbol characters and return key in STENO-mode.
## [0.0.4] - 2021-03-21
### Added
- This CHANGELOG file.
- User configurable bluetooth device name using main.plugins.plover_link.bt_device_name.
- User configurable list of bluetooth mac addresses, in order of priority, to auto-connect to using main.plugins.plover_link.bt_autoconnect_mac.
- User configurable option to clear eINK display at shutdown using ui.display.clear_at_shutdown.
- User configurable wpm calculation method using main.plugins.plover_link.wpm_method.
- User configurable wpm update frequency and calculation window in seconds using main.plugins.plover_link.wpm_timeout.
- More variety in mood indicators on common events.
- Requirements file.
### Changed
- Improved installation guide and documentation in README.
- All functionality in buttonshim plugin reworked into class for better integration with the project.
- More consistent logging messages.
- Stenogotchi_link version upgrade to v0.0.4.
### Fixed
- Reboot after initial setup or hostname change not working.
- Wifi status not showing as [OFF] if wifi is disabled at boot.
- Mode not changing to STENO when Plover becomes operational.
- All button press events not producing logging messages.
- Dependencies for stenogotchi_link Plover plugin corrected in setup.cfg.
## [0.0.3] - 2021-03-18
### Added
- First public pre-release version on GitHub.
- Stenogotchi, portable stenography using Plover and bluetooth keyboard emulation on a Raspberry Pi Zero W. With support for Waveshare 2.13 v2, ButtonSHIM and UPS-Lite v1.2 modules.
- Plover plugin stenogotchi_link for communicating between Plover and Stenogotchi.
- README now includes tested installation guide no longer requiring building PyQt5 from source.
- README now includes basic configuration and usage documentation.
- LICENSE file.
[Unreleased]: https://github.com/Anodynous/stenogotchi/compare/v0.1.0...dev
[0.1.0]: https://github.com/Anodynous/stenogotchi/compare/v0.0.5...v0.1.0
[0.0.5]: https://github.com/Anodynous/stenogotchi/compare/v0.0.4...v0.0.5
[0.0.4]: https://github.com/Anodynous/stenogotchi/compare/v0.0.3...v0.0.4
[0.0.3]: https://github.com/Anodynous/stenogotchi/releases/tag/v0.0.3
================================================
FILE: LICENSE
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
================================================
FILE: README.md
================================================
# Stenogotchi

Aim of the project is to deliver a cheap and portable device for running [Plover](https://www.openstenoproject.org/ "Plover: Open Steno Project") where local installation on the host is impossible or simply not preferred. A stand-alone link enabling stenography using any input device supported by Plover on any device accepting bluetooth keyboards.
Likely use-cases include:
- Mobile devices
- Corporate and public computers restricting software installations
- Hassle-free switching between devices without the need to install and configure Plover
- On-the-go stenographic recording
Stenogotchi is built on top of [Pwnagotchi](https://github.com/evilsocket/pwnagotchi), but instead of hungering for WPA handshakes it feeds on your steno chords. It emulates a BT HID device for connecting to a host and output can be toggled between STENO and QWERTY mode on the fly. The friendly UI optimized for low-power eINK displays is also accessible as a web UI version, making both the eINK display and buttonSHIM modules optional. If the RPI0w always will be powered over microUSB a separate battery pack is not needed. The suggested UPS-Lite 1000 mAH battery provides 3+ hours of runtime and supports pass-through charging.
## Hardware
| Module | Status |
|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------|
| [Raspberry Pi Zero W](https://www.raspberrypi.org/products/raspberry-pi-zero-w/) or [Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) | Required |
| MicroSD card (min 4 GiB) | Required |
| [Waveshare 2.13 v.2](https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT) | Recommended |
| [ButtonSHIM](https://shop.pimoroni.com/products/button-shim) | Recommended |
| [UPS-Lite v1.2](https://hackaday.io/project/173847-ups-lite) | Recommended |
| [DS3231 RTC Module](https://www.pishop.us/product/ds3231-real-time-clock-module-for-raspberry-pi/) | Optional |
See the [build notes](BUILDNOTES.md) for guidance on fitting the parts together.
## Installation
All commands should be executed as root. The installation process can be completed headless.
1. Flash and configure DietPi image, see https://dietpi.com/docs/install/
* For headless installation, set AUTO_SETUP_NET_WIFI_ENABLED=1 in dietpi.txt and enter wifi credentials in dietpi-wifi.txt before first boot.
* When prompted allow dietpi to update, change passwords and disable serial console. Under dietpi-config > advanced options enable:
* Bluetooth
* SPI state (needed by eINK screen)
* I2C state (needed by Buttonshim module and for UPS-Lite power readings)
* Using command 'dietpi-autostart' enable automatic login of user 'root' to local terminal (option #7)
2. Install dependencies
apt-get install git xorg xserver-xorg-video-fbdev python3-pip python3-rpi.gpio python3-gi libtiff5 libopenjp2-7 bluez screen rfkill -y
pip3 install file_read_backwards flask flask-wtf flask-cors evdev python-xlib pillow spidev jsonpickle dbus-python toml
3. Download and install Plover (v4.0.0.dev10)
wget https://github.com/openstenoproject/plover/releases/download/v4.0.0.dev10/plover-4.0.0.dev10-py3-none-any.whl
pip3 install plover-4.0.0.dev10-py3-none-any.whl
* If you'd rather try the [continuous build of Plover](https://github.com/openstenoproject/plover/releases/tag/continuous) for the latest improvements, you will need to install both build-essential and python3-dev through apt-get first. Switch back to dev10 if you experience issues.
4. Clone the Stenogotchi repository and install the plover plugin "stenogotchi_link"
git clone https://github.com/Anodynous/stenogotchi.git
pip3 install ./stenogotchi/plover_plugin/
5. Configure Plover. Setup will ultimately depend on your own preferences and keyboard, but below is what I use. Make sure to include at least 'auto_start = True' and the '[Plugins]' section in your own config.
mkdir -p /root/.config/plover/
nano /root/.config/plover/plover.cfg
#----------
[Output Configuration]
space_placement = After Output
start_attached = True
start_capitalize = False
undo_levels = 30
[Logging Configuration]
enable_stroke_logging = False
enable_translation_logging = False
[Machine Configuration]
auto_start = True
machine_type = Gemini PR
[Gemini PR]
baudrate = 9600
bytesize = 8
parity = N
port = /dev/ttyACM0
stopbits = 1
timeout = 2.0
[Plugins]
enabled_extensions = ["stenogotchi_link"]
6. Run installation script to finalize setup of Stenogotchi. Optional boot time improvements offered by script described in "Significantly reduce boot time" section below.
chmod +x ./stenogotchi/initial_setup.sh
./stenogotchi/initial_setup.sh
7. Configure Stenogotchi settings after reboot completes
nano /etc/stenogotchi/config.toml
#----------modify the config as you see fit----------#
main.plugins.buttonshim.enabled = true
main.plugins.upslite.enabled = true
main.plugins.evdevkb.enabled = true
main.plugins.plover_link.bt_autoconnect_mac = '00:DE:AD:BE:EF:00,11:DE:AD:BE:EF:11'
#----------
## Updating
cd ~/stenogotchi
git pull
pip3 install ~/stenogotchi/plover_plugin/
## Configuration / Troubleshooting
* Configuration files are placed in /etc/stenogotchi/. Create a separate file named config.toml containing overrides to the defaults. Don't edit default.toml directly as it will be overwritten on Stenogotchi version updates.
* The logfile is created in /var/log, which dietpi by default mounts to RAM to preserve the SD card lifespan. To make the file persistent across reboots it needs to be written to the disk. To aid with troubleshooting, either set the location to another existing folder using 'main.log.path' in config.toml or change the global dietpi setting using dietpi-software > Log System.
* If your target device expects a different input language or keyboard layout than US qwerty, use setxkbmap to align it. This should be added to the beginning of your .xinitrc file to run automatically at startup. For German language and dvorak layout for example the below would be used.
setxkbmap -layout de -variant dvorak
### Bluetooth connections
* Define your bluetooth devices in main.plugins.plover_link.bt_autoconnect_mac to auto-connect on boot. Multiple comma-separated devices in order of priority can be given. If no connection attempts are successful at boot, the device will fall back to listening for incoming connection and pairing attempts.
* Only one active connection at a time is supported. To switch remote devices, disable bluetooth on the remote device and wait around 10 seconds before initiating a new connection. The Stenogotchi will attempt to reconnect to the lost device for a few seconds before falling back to listening for new incoming connections.
* Issues with pairing or connecting after changes in bluetooth configurations can normally be fixed through unpairing and re-pairing. On the Stenogotchi side this is best handled through bluetoothctl using the below process. Re-initiate pairing process from remote device after the pairing information has been cleared on both host and client side.
bluetoothctl
[bluetooth]# paired-devices
Device 00:DE:AD:BE:EF:00 Anodynous' Ipad
[bluetooth]# remove 00:DE:AD:BE:EF:00
[bluetooth]# exit
### Significantly reduce boot time
* Set ARM initial turbo to the max (60s) under dietpi-config > performance options to reduce boot time. You can also play around with overclocking, throttling and cpu governor to find a suitable balance between performance and power draw.
* Disable dietpi and apt update check at boot:
nano /boot/dietpi.txt
#----------
CONFIG_CHECK_DIETPI_UPDATES=0
CONFIG_CHECK_APT_UPDATES=0
* Disable waiting for network and time sync at boot. Doing this you should be aware that the RPI0w does not have a hardware clock. It will lose track of real world time as soon it is powered off, making log timestamps or any time based action you may set up unreliable. None of this is important for the core functionality of the Stenogotchi and disabling time-sync at boot can shave up to a minute off the boot process. By adding a cheap I2C hardware clock you can completely remove the need for network sync. Many modules are small enough to fit in the empty space of the UPS-Lite or under the eINK screen. See the [build notes](BUILDNOTES.md) for more directions.
nano /boot/dietpi.txt
#----------
CONFIG_BOOT_WAIT_FOR_NETWORK=0
CONFIG_NTP_MODE=0
## Usage

### Buttonshim
Below long-press (1s) actions are pre-defined. Short-press triggers user configurable terminal commands, e.g. rclone sync of Plover dictionaries with cloud storage.
* Button A - toggle QWERTY / STENO mode
* Button B - toggle wpm & strokes readings
* Button C - toggle dictionary lookup mode
* Button D - toggle wifi (reboot persistent)
* Button E - shutdown
## Project roadmap
- [x] Proof of concept. Headless RPI0W running Plover, emulating bluetooth HID device and seamlessly piping steno output over BT to host
- [x] Create proper plugin for integration with Plover
- [x] Integrate bluetooth HID server as Stenogotchi plugin
- [x] Support for eINK display and web UI
- [x] Support for buttons for built-in and user customizable actions
- [x] Support for external battery charge readings on UI
- [x] Create Stenogotchi plugin to capture QWERTY input while blocking Plover
- [x] ButtonSHIM toggle to enable/disable WIFI, persisting reboot
- [x] ButtonSHIM toggle between STENO and QWERTY output mode
- [x] WPM readings for STENO mode
- [x] Full installation guide for Plover and Stenogotchi using DietPi as base image
- [x] Dictionary lookup using eINK screen
- [x] Decrease steno latency
- [x] Improved web UI, including buttonSHIM functionality
- [ ] Plover chords for triggering Stenogotchi actions
- [ ] Dictionary additions using eINK screen
- [ ] Document configuration options and buttonSHIM functionality
- [ ] Clean up and optimize code, fix bugs and add test suite
- [ ] Expand Stenogotchi statuses, reactions and mood indicators
- [ ] On-the-fly updating and reloading of Plover dictionaries
- [ ] Simple AI for shaping personality of the Stenogotchi
- [ ] Proper usage and configuration documentation
- [ ] Support for other eINK display modules
## Pictures



## License
Released under the GPL3 license.
================================================
FILE: initial_setup.sh
================================================
#!/bin/bash
# Finalize setup of Stenogotchi. Only needs to be run once.
# Use the below steps if you prefer not executing the automated script or run into issues.
# ------------------------------------------------------------------------------------------
# 1) Add configuration file for D-Bus service used by Stenogotchi and the Plover plugin to communicate
#cp ./stenogotchi/plover_plugin/stenogotchi_link/com.github.stenogotchi.conf /etc/dbus-1/system.d/
# 2) Modify service file to remove input bluetooth plugin so it does not grab the sockets Stenogotchi requires access to.
# Append '-P input' to existing line in '/lib/systemd/system/bluetooth.service' file to end up with:
#ExecStart=/usr/libexec/bluetooth/bluetoothd -P input
# 3) Ensure 'root' user autologin to local terminal is enabled using dietpi-autostart
# 4) Create file '~/.bash_profile' with the below content (excluding #-characters)
#if [[ -z $DISPLAY ]] && [[ $(tty) = /dev/tty1 ]]; then
# screen -S stenogotchi -dm python3 /root/stenogotchi/stenogotchi.py --debug
# xinit
#fi
# 5) Create file '~/.xinitrc' with the below content
#screen -S plover plover -g none -l debug
# 6) Launch Stenogotchi manually once which will complete setup and reboot device.
# Configure settings per preference in '/etc/stenogotchi/config.toml' after reboot.
# 7) Optional but highly recommended. Reduce boot time by disabling Dietpi and APT update check and setting initial turbo boost to 60s.
# In /boot/dietpi.txt set below values:
#CONFIG_CHECK_DIETPI_UPDATES=0
#CONFIG_CHECK_APT_UPDATES=0
# In /boot/config.txt set:
#initial_turbo=60
# 8) Optional but recommended. Reduce boot time further by disabling waiting for network and NTP time sync.
# Doing this you should be aware that the RPI0w does not have a hardware clock. It will lose track of real world time as soon it is powered off, making log timestamps or any time based action you may set up unreliable. None of this is important for the core functionality of the Stenogotchi and disabling time-sync at boot can shave up to a minute off the boot process. By adding a cheap I2C hardware clock you can completely remove the need for network sync.
# In /boot/dietpi.txt set below values:
#CONFIG_BOOT_WAIT_FOR_NETWORK=0
#CONFIG_NTP_MODE=0
# 9) Launch stenogotchi manually and configure using /etc/stenogotchi/config.toml after reboot has completed
#python3 ./stenogotchi/stenogotchi.py
FLAG="/var/log/_stenogotchi_setup_completed.log"
BASEDIR=$(cd `dirname $0` && pwd)
# Check and run script only if it hasn't been done already
if [ ! -f $FLAG ]; then
# Set flag indicating script has been run
touch $FLAG
# Configure bluetooth service
printf "Adding configuration file for D-Bus service used by Stenogotchi and the Plover plugin to communicate.\n"
sleep 1
cp $BASEDIR/plover_plugin/stenogotchi_link/com.github.stenogotchi.conf /etc/dbus-1/system.d/ && printf "Success!\n" || printf "Failed! \nPlease manually copy 'com.github.stenogotchi.conf' from stenogotchi_link folder to '/etc/dbus-1/system.d/'"
printf "\nModifying service file to remove input bluetooth plugin so it does not grab the sockets Stenogotchi requires access to.\n"
sleep 1
# Only add in case not appended yet
if ! grep -q "\-P input" /lib/systemd/system/bluetooth.service; then
sed '/^ExecStart=/s/$/ -P input/' /lib/systemd/system/bluetooth.service -i.bkp && printf "Success!\n" || printf "Failed! \nPlease manually modify existing line in '/lib/systemd/system/bluetooth.service' to 'ExecStart=/usr/lib/bluetooth/bluetoothd -P input' "
else
printf "Skipped! Bluetooth input plugin already disabled\n"
fi
# Configure autostart of plover
printf "\nConfigure Stenogotchi to auto-start at boot. Will run under screen session named 'stenogotchi'\n"
sleep 1
echo "screen -S plover plover -g none -l debug" > ~/.xinitrc && printf "Success!\n" || printf "Failed! \nPlease manually create file '~/.xinitrc' with contents 'screen -S plover plover -g none -l debug'"
# Configure autostart of stenogotchi
read -r -d '' AUTOSTART << "EOM"
if [[ -z $DISPLAY ]] && [[ $(tty) = /dev/tty1 ]]; then
screen -S stenogotchi -dm python3 /root/stenogotchi/stenogotchi.py --debug
xinit
fi
EOM
printf "\nConfigure Plover to auto-start at boot. Will run under screen session named 'plover'\n"
sleep 1
echo "$AUTOSTART" > ~/.bash_profile && printf "Success!\n" || printf "Failed! \nPlease manually create '~/.bash_profile' file with contents \n"$AUTOSTART" "
# Optionally: Optimize boot time
printf "\nReduce boot time by disabling automatic Dietpi and APT update checks. Set initial turbo boost to 60s."
printf "\nSettings can be changed later in '/boot/dietpi.txt' and '/boot/config.txt'\n"
while true; do
read -p "Do you wish to enable these highly recommended optimizations? (y/n)" yn
case $yn in
[Yy]* )
sed -i 's/CONFIG_CHECK_DIETPI_UPDATES=.*/CONFIG_CHECK_DIETPI_UPDATES=0/' /boot/dietpi.txt && printf "set CONFIG_CHECK_DIETPI_UPDATES=0 in /boot/dietpi.txt\n";
sed -i 's/CONFIG_CHECK_APT_UPDATES=.*/CONFIG_CHECK_APT_UPDATES=0/' /boot/dietpi.txt && printf "set CONFIG_CHECK_APT_UPDATES=0 in /boot/dietpi.txt\n";
sed -i 's/initial_turbo=.*/initial_turbo=60/' /boot/config.txt && printf "set initial_turbo=60 in /boot/config.txt\n";
break;;
[Nn]* )
printf "Boot optimization skipped. Enable later by setting CONFIG_CHECK_DIETPI_UPDATES=0 and CONFIG_CHECK_APT_UPDATES=0 in /boot/dietpi.txt and initial_turbo=60 in /boot/config.txt\n";
break;;
* ) echo "Please answer yes or no.";;
esac
done
# Optionally: Disable waiting for network and NTP-sync
printf "\nDisable waiting for network and NTP-sync at boot to significantly reduce boot time when networks are unavailable."
printf "\nSettings can be changed later in '/boot/dietpi.txt'\n"
while true; do
read -p "Do you wish to enable these optimizations? (y/n)" yn
case $yn in
[Yy]* )
sed -i 's/CONFIG_BOOT_WAIT_FOR_NETWORK=.*/CONFIG_BOOT_WAIT_FOR_NETWORK=0/' /boot/dietpi.txt && printf "set CONFIG_BOOT_WAIT_FOR_NETWORK=0 in /boot/dietpi.txt\n";
sed -i 's/CONFIG_NTP_MODE=.*/CONFIG_NTP_MODE=0/' /boot/dietpi.txt && printf "set CONFIG_NTP_MODE=0 in /boot/dietpi.txt\n";
break;;
[Nn]* )
printf "Boot optimization skipped. Enable later by setting CONFIG_BOOT_WAIT_FOR_NETWORK=0 and CONFIG_NTP_MODE=0 in /boot/dietpi.txt\n";
break;;
* ) echo "Please answer yes or no.";;
esac
done
# Initial launch of Stenogotchi which triggers reboot
printf "\nFirst time launch of Stenogotchi. Configure settings in /etc/stenogotchi/config.toml after reboot\n"
sleep 5
python3 $BASEDIR/stenogotchi.py
else
printf "Setup script has already been executed and should not need to be run again. (Delete '/var/log/_stenogotchi_setup_completed.log' to bypass check)\n"
fi
================================================
FILE: plover_plugin/setup.cfg
================================================
[metadata]
name = stenogotchi_link
version = 0.3.0
description = A plugin for exposing Plover events and communicating with Stenogotchi over D-Bus
long_description =
author = Anodynous
author_email =
license = GNU General Public License v3 or later (GPLv3+)
url = https://github.com/Anodynous/Stenogotchi
classifiers =
Development Status :: 4 - Beta
Environment :: Plugins
Intended Audience :: End Users/Desktop
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Operating System :: Linux
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
keywords = plover plover_plugin stenogotchi
[options]
zip_safe = True
install_requires =
plover>=4.0.0.dev10
jsonpickle
dbus-python
textstat
PyGObject
packages =
stenogotchi_link
[options.entry_points]
plover.extension =
stenogotchi_link = stenogotchi_link.stenogotchi_link:EngineServer
================================================
FILE: plover_plugin/setup.py
================================================
from setuptools import setup
setup()
================================================
FILE: plover_plugin/stenogotchi_link/__init__.py
================================================
================================================
FILE: plover_plugin/stenogotchi_link/clients.py
================================================
#!/usr/bin/env python3
import plover.log
import dbus, dbus.exceptions
from dbus.mainloop.glib import DBusGMainLoop
from time import sleep
from threading import Thread
from gi.repository import GLib
from Xlib import X, XK
from plover.oslayer.xkeyboardcontrol import KeyboardEmulation, uchr_to_keysym
from plover import key_combo as plover_key_combo
from stenogotchi_link.keymap import plover_convert, plover_modkey
SERVER_DBUS = 'com.github.stenogotchi'
SERVER_SRVC = '/com/github/stenogotchi'
ERROR_NO_SERVER: str = 'A server is not currently running'
ERROR_SERVER_RUNNING: str = 'A server is already running'
TIME_SLEEP = 0
class StenogotchiClient:
"""
Transmits Plover event updates to, and listens for signals from, Stenogotchi over D-Bus.
"""
def __init__(self, engineserver):
self._engineserver = engineserver
self._setup_dbus_loop()
self._setup_object()
def _setup_dbus_loop(self):
DBusGMainLoop(set_as_default=True)
self._mainloop = GLib.MainLoop()
self._thread = Thread(target=self._mainloop.run)
self._thread.start()
def _setup_object(self):
try:
self.bus = dbus.SystemBus()
self.stenogotchiobject = self.bus.get_object(SERVER_DBUS, SERVER_SRVC)
self.stenogotchi_service = dbus.Interface(self.stenogotchiobject, SERVER_DBUS)
# Add signal receiver for incoming messages
self.stenogotchi_signal = self.bus.add_signal_receiver(path=SERVER_SRVC,
handler_function=self.stenogotchi_signal_handler,
dbus_interface=SERVER_DBUS,
signal_name='signal_to_plover')
except dbus.exceptions.DBusException as e:
plover.log.error(f'[stenogotchi_link] Failed to initialize D-Bus object: {str(e)}')
def _exit(self):
self._mainloop.quit()
def plover_is_running(self, b):
self.stenogotchi_service.plover_is_running(b)
# If plover is shutting down, quit mainloop
if not b:
self._exit()
def plover_is_ready(self, b):
self.stenogotchi_service.plover_is_ready(b)
def plover_machine_state(self, s):
self.stenogotchi_service.plover_machine_state(s)
def plover_output_enabled(self, b):
self.stenogotchi_service.plover_output_enabled(b)
def plover_wpm_stats(self, s):
self.stenogotchi_service.plover_wpm_stats(s)
def plover_strokes_stats(self, s):
self.stenogotchi_service.plover_strokes_stats(s)
def send_backspaces(self, y):
self.stenogotchi_service.send_backspaces_stenogotchi(y)
def send_string(self, s):
self.stenogotchi_service.send_string_stenogotchi(s)
def send_key_combination(self, s):
self.stenogotchi_service.send_key_combination_stenogotchi(s)
def send_lookup_results(self, l):
self.stenogotchi_service.plover_translation_handler(l)
def stenogotchi_signal_handler(self, dict):
# Enable and disable wpm/strokes meters
if 'lookup_word' in dict:
self._engineserver.lookup_word(dict['lookup_word'])
if 'lookup_stroke' in dict:
self._engineserver.lookup_stroke(dict['lookup_stroke'])
if 'output_to_stenogotchi' in dict:
self._engineserver._output_to_stenogotchi = dict['output_to_stenogotchi']
if 'start_wpm_meter' in dict:
wpm_method = dict['wpm_method']
wpm_timeout = int(dict['wpm_timeout'])
plover.log.info('[stenogotchi_link] Starting WPM meter')
if dict['start_wpm_meter'] == 'wpm and strokes':
self._engineserver.start_wpm_meter(enable_wpm=True, enable_strokes=True, wpm_method=wpm_method, wpm_timeout=wpm_timeout)
elif dict['start_wpm_meter'] == 'wpm':
self._engineserver.start_wpm_meter(enable_wpm=True, enable_strokes=False, wpm_method=wpm_method, wpm_timeout=wpm_timeout)
elif dict['start_wpm_meter'] == 'strokes':
self._engineserver.start_wpm_meter(enable_wpm=False, enable_strokes=True, wpm_method=wpm_method, wpm_timeout=wpm_timeout)
if 'stop_wpm_meter' in dict:
plover.log.info('[stenogotchi_link] Stopping WPM meter')
if dict['stop_wpm_meter'] == 'wpm and strokes':
self._engineserver.stop_wpm_meter(disable_wpm=True, disable_strokes=True)
elif dict['stop_wpm_meter'] == 'wpm':
self._engineserver.stop_wpm_meter(disable_wpm=True, disable_strokes=False)
elif dict['stop_wpm_meter'] == 'strokes':
self._engineserver.stop_wpm_meter(disable_wpm=False, disable_strokes=True)
class CustomKeyboardEmulation(KeyboardEmulation):
"""
We use the Plover implementation, but change _update_keymap()
to prefer lower keycode rather than low modkeys combos in mapping.
This way we avoid some issues when mapping to BT HID keycodes.
"""
def _update_keymap(self):
'''Analyse keymap, build a mapping of keysym to (keycode + modifiers),
and find unused keycodes that can be used for unmapped keysyms.
'''
self._keymap = {}
self._custom_mappings_queue = []
# Analyse X11 keymap.
keycode = self._display.display.info.min_keycode
keycode_count = self._display.display.info.max_keycode - keycode + 1
for mapping in self._display.get_keyboard_mapping(keycode, keycode_count):
mapping = tuple(mapping)
while mapping and X.NoSymbol == mapping[-1]:
mapping = mapping[:-1]
if not mapping:
# Free never used before keycode.
custom_mapping = [self.UNUSED_KEYSYM] * self.CUSTOM_MAPPING_LENGTH
custom_mapping[-1] = self.PLOVER_MAPPING_KEYSYM
mapping = custom_mapping
elif self.CUSTOM_MAPPING_LENGTH == len(mapping) and \
self.PLOVER_MAPPING_KEYSYM == mapping[-1]:
# Keycode was previously used by Plover.
custom_mapping = list(mapping)
else:
# Used keycode.
custom_mapping = None
for keysym_index, keysym in enumerate(mapping):
if keysym == self.PLOVER_MAPPING_KEYSYM:
continue
if keysym_index not in (0, 1, 4, 5):
continue
modifiers = 0
if 1 == (keysym_index % 2):
# The keycode needs the Shift modifier.
modifiers |= X.ShiftMask
if 4 <= keysym_index <= 5:
# 3rd (AltGr) level.
modifiers |= X.Mod5Mask
mapping = self.Mapping(keycode, modifiers, keysym, custom_mapping)
if keysym != X.NoSymbol and keysym != self.UNUSED_KEYSYM:
# Some keysym are mapped multiple times, prefer lower keycode (Plover prefers lower modifiers combos).
previous_mapping = self._keymap.get(keysym)
if previous_mapping is None or mapping.keycode < previous_mapping.keycode:
self._keymap[keysym] = mapping
if custom_mapping is not None:
self._custom_mappings_queue.append(mapping)
keycode += 1
# Determine the backspace mapping.
backspace_keysym = XK.string_to_keysym('BackSpace')
self._backspace_mapping = self._get_mapping(backspace_keysym)
assert self._backspace_mapping is not None
assert self._backspace_mapping.custom_mapping is None
# Get modifier mapping.
self.modifier_mapping = self._display.get_modifier_mapping()
class BTClient:
"""
Transmits keystroke output from Plover to Stenogotchi as HID messages over D-Bus.
"""
def __init__(self):
self.target_length = 6
self.mod_keys = 0b00000000
self.pressed_keys = []
self.bus = dbus.SystemBus()
self.btkobject = self.bus.get_object(SERVER_DBUS, SERVER_SRVC)
self.btk_service = dbus.Interface(self.btkobject, SERVER_DBUS)
self.ke = CustomKeyboardEmulation()
self._backspace_mapping_hid = plover_convert(self.ke._backspace_mapping.keycode)
def update_mod_keys(self, mod_key, value):
"""
Which modifier keys are active is stored in an 8 bit number.
Each bit represents a different key. This method takes which bit
and its new value as input
:param mod_key: The value of the bit to be updated with new value
:param value: Binary 1 or 0 depending if pressed or released
"""
bit_mask = 1 << (7-mod_key)
if value: # set bit
self.mod_keys |= bit_mask
else: # clear bit
self.mod_keys &= ~bit_mask
def update_keys(self, norm_key, value):
"""
Sets the active normal keys
"""
if norm_key < 0: # Log for debug purposes in case keycode isn't valid
plover.log.error(f"[stenogotchi_link] KeyError in update_keys, unable to send keycode: {norm_key}")
return
if value < 1:
self.pressed_keys.remove(norm_key)
elif norm_key not in self.pressed_keys:
self.pressed_keys.insert(0, norm_key)
len_delta = self.target_length - len(self.pressed_keys)
if len_delta < 0:
self.pressed_keys = self.pressed_keys[:len_delta]
elif len_delta > 0:
self.pressed_keys.extend([0] * len_delta)
@property
def state(self):
"""
property with the HID message to be sent
:return: bytes of HID message
"""
return [0xA1, 0x01, self.mod_keys, 0, *self.pressed_keys]
def clear_mod_keys(self):
self.mod_keys = 0b00000000
def clear_keys(self):
self.pressed_keys = []
def send_keys(self, state_list=None):
if not state_list:
self.btk_service.send_keys([state_list])
else:
flat_list = state_list
self.btk_service.send_keys(flat_list)
def send_backspaces(self, number_of_backspaces):
self.clear_keys()
self.clear_mod_keys()
state_list = []
for x in range(number_of_backspaces):
self.update_keys(self._backspace_mapping_hid, 1)
state_list.append(self.state)
self.update_keys(self._backspace_mapping_hid, 0)
state_list.append(self.state)
self.send_keys(state_list)
def map_hid_events(self, keycode, modifiers=None):
""" Returns a list of HID bytearrays to produce the key combination.
Arguments:
keycode -- An integer in the inclusive range [8-255].
modifiers -- An 8-bit bit mask indicating if the key
pressed is modified by other keys, such as Shift, Capslock,
Control, and Alt.
TODO: International, accented characters and a range of symbols are not working with the US keyboard layout.
Could be solved using Alt codes on Windows, but Mac/iOS uses different Option codes. Complicated to solve
in a clean way as we only emulate a BT keyboard and all characters must be produced on the remote host.
-> Look into separate WIN/MAC translation tables. Profiles which can be linked to BT MAC adresses.
"""
self.clear_keys()
self.clear_mod_keys()
state_sublist = []
#modifiers_list = [
# self.ke.modifier_mapping[n][0]
# for n in range(8)
# if (modifiers & (1 << n))
#]
#for mod_keycode in modifiers_list:
# self.update_mod_keys(plover_modkey(mod_keycode), 1)
normkey_hid = plover_convert(keycode)
# Log issues with mapping any keycodes
if normkey_hid < 0:
plover.log.error(f"[stenogotchi_link] Unable to map keycode: {keycode} using plover_convert in keymap.py.")
return
if modifiers:
modkey_hid = plover_modkey(modifiers)
if modkey_hid < 0:
plover.log.error(f"[stenogotchi_link] Unable to map keycode: {keycode} using plover_modkey in keymap.py.")
# Apply modifiers
if modifiers:
# TODO: Handle combination of modifiers. 129 for example is shift (1) + AltGr (128)
self.update_mod_keys(modkey_hid, 1)
# Press and release the base key.
self.update_keys(normkey_hid, 1)
state_sublist.append(self.state)
sleep(TIME_SLEEP)
self.update_keys(normkey_hid, 0)
state_sublist.append(self.state)
# Release modifiers
if modifiers > 0:
self.update_mod_keys(modkey_hid, 0)
state_sublist.append(self.state)
return state_sublist
#for mod_keycode in reversed(modifiers_list):
# self.update_mod_keys(plover_modkey(mod_keycode), 0)
def send_string(self, s):
state_list = []
for char in s:
keysym = uchr_to_keysym(char)
mapping = self.ke._get_mapping(keysym)
#plover.log.debug(f"[stenogotchi_link] mapping : '{mapping}' mapping.keycode : '{mapping.keycode}' and mapping.modifier : '{mapping.modifiers}' (for keysym : '{keysym}' given char : '{char}')")
if mapping is None:
continue
sublist = self.map_hid_events(mapping.keycode, mapping.modifiers)
if sublist:
state_list.extend(sublist)
if len(state_list) > 0:
self.send_keys(state_list)
def send_key_combination(self, combo_string: str):
"""
Custom implementation of Plover send_key_combination function
since we do not work with emulated key events or need to fight
with a KeyboardCapture instance.
Arguments:
combo_string -- A string representing a sequence of key
combinations. Keys are represented by their names in the
Xlib.XK module, without the 'XK_' prefix. For example, the
left Alt key is represented by 'Alt_L'. Keys are either
separated by a space or a left or right parenthesis.
Parentheses must be properly formed in pairs and may be
nested. A key immediately followed by a parenthetical
indicates that the key is pressed down while all keys enclosed
in the parenthetical are pressed and released in turn. For
example, Alt_L(Tab) means to hold the left Alt key down, press
and release the Tab key, and then release the left Alt key.
"""
self.clear_keys()
self.clear_mod_keys()
state_list = []
# Parse and validate combo.
key_events = [
(keycode, 1 if pressed else 0) for keycode, pressed
in plover_key_combo.parse_key_combo(combo_string, self.ke._get_keycode_from_keystring)
]
# Send key events to emulate combination.
for keycode, event_type in key_events:
# Convert to HID
normkey_hid = plover_convert(keycode)
if normkey_hid == -1:
modkey_hid = plover_modkey(keycode)
else:
modkey_hid = -1
# Update and send keycode if mapped, otherwise log
if modkey_hid > -1:
self.update_mod_keys(modkey_hid, event_type)
state_list.append(self.state)
elif normkey_hid > -1:
self.update_keys(normkey_hid, event_type)
state_list.append(self.state)
else:
plover.log.debug(f"[stenogotchi_link]Received key_combination from Plover: {combo_string}, resulting in key_events: {key_events}")
plover.log.error(f"Unable to map keycode: {keycode}, in keymap.py (event_type: {event_type})")
self.send_keys(state_list)
================================================
FILE: plover_plugin/stenogotchi_link/com.github.stenogotchi.conf
================================================
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy context="default">
<allow own="com.github.stenogotchi"/>
<allow send_destination="com.github.stenogotchi"/>
</policy>
</busconfig>
================================================
FILE: plover_plugin/stenogotchi_link/keymap.py
================================================
# Find HID keycode mapping here: https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf
# Linux keycodes can be found in /usr/share/X11/xkb/keycodes/evdev
# Linux keycode used by Plover : HID keycode used for BT
plover_keytable = {
# Function row.
67: 58, #"F1",
68: 59, #"F2",
69: 60, #"F3",
70: 61, #"F4",
71: 62, #"F5",
72: 63, #"F6",
73: 64, #"F7",
74: 65, #"F8",
75: 66, #"F9",
76: 67, #"F10",
95: 68, #"F11",
96: 69, #"F12",
# Number row.
49: 53, #"`",
10: 30, #"1",
11: 31, #"2",
12: 32, #"3",
13: 33, #"4",
14: 34, #"5",
15: 35, #"6",
16: 36, #"7",
17: 37, #"8",
18: 38, #"9",
19: 39, #"0",
20: 45, #"-",
21: 46, #"=",
51: 50, #"\\",
# Upper row.
24: 20, #"q",
25: 26, #"w",
26: 8, #"e",
27: 21, #"r",
28: 23, #"t",
29: 28, #"y",
30: 24, #"u",
31: 12, #"i",
32: 18, #"o",
33: 19, #"p",
34: 47, #"[",
35: 48, #"]",
# Home row.
38: 4, #"a",
39: 22, #"s",
40: 7, #"d",
41: 9, #"f",
42: 10, #"g",
43: 11, #"h",
44: 13, #"j",
45: 14, #"k",
46: 15, #"l",
47: 51, #";",
48: 52, #"'",
# Bottom row.
52: 29, #"z",
53: 27, #"x",
54: 6, #"c",
55: 25, #"v",
56: 5, #"b",
57: 17, #"n",
58: 16, #"m",
59: 54, #",",
60: 55, #".",
61: 56, #"/",
# Other keys.
22 : 42, #"BackSpace",
119: 76, #"Delete",
116: 81, #"Down",
115: 77, #"End",
9 : 41, #"Escape",
110: 74, #"Home",
118: 73, #"Insert",
113: 80, #"Left",
117: 78, #"Page_Down",
112: 75, #"Page_Up",
36 : 40, #"Return",
114: 79, #"Right",
23 : 43, #"Tab",
111: 82, #"Up",
65 : 44, #"Space",
66 : 57, #Caps_Lock,
#"KEY_BACK": 241,
#"KEY_FORWARD": 242,
#"KEY_REFRESH": 250,
#"KEY_SYSRQ": 70,
# Keypad keys
#"KEY_NUMLOCK": 83,
#"KEY_SCROLLLOCK": 71,
79: 95, #kp_7 - "KEY_KP7"
80: 96, #kp_8 - "KEY_KP8"
81: 97, #kp_9 - "KEY_KP9"
#"KEY_KPMINUS": 86,
83: 92, #kp_4 - "KEY_KP4"
84: 93, #kp_5 - "KEY_KP5"
85: 94, #kp_6 - "KEY_KP6"
#"KEY_KPPLUS": 87,
87: 89, #kp_1 - "KEY_KP1"
88: 90, #kp_2 - "KEY_KP2"
89: 91, #kp_3 - "KEY_KP3"
90: 98, #kp_0 - "KEY_KP0"
126: 215, # "Keypad ±"
#"KEY_KPDOT": 99,
# TODO: Media keys not working correctly
# Media keys
172: 232, # AudioPlay - "KEY_PLAYPAUSE"
209: 232, # AudioPause - "KEY_PLAYPAUSE"
121: 239, # AudioMute - "KEY_MUTE"
122: 238, #AudioLowerVolume - "KEY_VOLUMEDOWN"
123: 237, #AudioRaiseVolume - "KEY_VOLUMEUP"
173: 234, #AudioPrev - "KEY_PREVIOUSSONG"
171: 235, #AudioNext - "KEY_NEXTSONG"
174: 233, #AudioStop - "KEY_STOPCD"
169: 236, #Eject - "KEY_EJECTCD"
}
# Map modifier keys to array element in the bit array
plover_modkeys = {
# TODO: AltGr will not on MacOS/iOs/iPadOS produce expected character.
134: 0, #Super_R - "KEY_RIGHTMETA", WIN-key
128: 1, #"KEY_RIGHTALT", AltGr used to produce some symbols
62: 2, #"KEY_RIGHTSHIFT"
105: 3, #"KEY_RIGHTCTRL"
133: 4, #Super_L - "KEY_LEFTMETA", WIN-key
64: 5, #"KEY_LEFTALT"
50 : 6, #"KEY_LEFTSHIFT"
1 : 6, #"KEY_LEFTSHIFT", mapped to both 50 and 1 as '1' is used by Plover as shift modifier to capitalize letters and '50' when sent in key-combo
37: 7 #"KEY_LEFTCTRL"
}
def plover_convert(plover_keycode):
if plover_keycode in plover_keytable:
return plover_keytable[plover_keycode]
else:
return -1 # Return an invalid keycode
def plover_modkey(plover_keycode):
if plover_keycode in plover_modkeys:
return plover_modkeys[plover_keycode]
else:
return -1 # Return an invalid array element
================================================
FILE: plover_plugin/stenogotchi_link/stenogotchi_link.py
================================================
#!/usr/bin/env python3
# This is a Plover plugin acting as link between Plover and Stenogotchi
# Based on: https://github.com/nsmarkop/plover_websocket_server
import plover.log
import json
import jsonpickle
from typing import Optional, List
from time import sleep
from plover.engine import StenoEngine
from plover.steno import Stroke
from plover.config import Config
from plover.formatting import _Action
from plover.steno_dictionary import StenoDictionaryCollection
from stenogotchi_link.clients import BTClient, StenogotchiClient
from stenogotchi_link.wpm import PloverWpmMeter, PloverStrokesMeter
ERROR_MISSING_ENGINE = 'Plover engine not provided'
class EngineServer():
"""
Hooks into Plover events and makes them available to Stenogotchi.
"""
# Called once to initialize an instance which lives until Plover exits
def __init__(self, engine: StenoEngine) -> None:
self._engine: StenoEngine = engine
self._stenogotchiclient = StenogotchiClient(self)
self._btclient = BTClient()
self._wpm_meter = None
self._strokes_meter = None
self._output_to_stenogotchi = False
# Started when user enables extension
def start(self):
""" Starts the server. """
self._connect_hooks()
plover.log.info("[stenogotchi_link] Plover_link started")
self._stenogotchiclient.plover_is_running(True)
# Called when Plover exits or user disables the extension
def stop(self):
""" Stops the server. """
self._disconnect_hooks()
self._stenogotchiclient.plover_is_running(False)
def start_wpm_meter(self, enable_wpm=False, enable_strokes=False, wpm_method='ncra', wpm_timeout=60):
""" Starts WPM and/or Strokes meters
"""
if enable_wpm:
self._wpm_meter = PloverWpmMeter(stenogotchi_link=self, wpm_method=wpm_method, timeout=wpm_timeout)
if enable_strokes:
self._strokes_meter = PloverStrokesMeter(stenogotchi_link=self, strokes_method=wpm_method, timeout=wpm_timeout)
def stop_wpm_meter(self, disable_wpm=True, disable_strokes=True):
if disable_wpm:
self._wpm_meter.quit()
self._wpm_meter = None
if disable_strokes:
self._strokes_meter.quit()
self._strokes_meter = None
def _on_wpm_meter_update_strokes(self, stats):
""" Sends strokes stats to stenogotchi as a string """
self._stenogotchiclient.plover_strokes_stats(stats['strokes_user'])
def _on_wpm_meter_update_wpm(self, stats):
""" Sends wpm stats to stenogotchi as a string """
self._stenogotchiclient.plover_wpm_stats(stats['wpm_user'])
def _on_plover_translation(self, results, type):
""" Sends translation results from Plover to stenogotchi as list of strings"""
if type == 'word': # Result from Plover will be in format List[Tuple[str]], examine: {('EBGS', 'APL', '*EUPB'), ('KP*PL',), ('KPAPL', 'PHEUPB'), ('KPAPL', '-PB'), ('KP-PB',), ('EBGS', 'APL', '-PB'), ('KP',), ('EBGS', 'APL', 'PHEUPB'), ('EBGS', 'APL', 'EUPB')}
results_list = []
chords = None
for touple in results:
if chords:
results_list.append(chords)
chords = None
for result in touple:
if chords:
chords = chords + '/' + result
else:
chords = result
if chords:
results_list.append(chords)
chords = None
elif type == 'stroke': # Result from Plover will be in format str
# TODO: implement functionality for stroke-lookup
plover.log.debug(f"Lookup_stroke results: {results}")
results_list = [results]
self._stenogotchiclient.send_lookup_results(results_list)
def lookup_word(self, word):
matches = self._engine.reverse_lookup(word)
self._on_plover_translation(matches, 'word')
def lookup_stroke(self, stroke):
matches = self._engine.lookup(stroke)
self._on_plover_translation(matches, 'stroke')
def get_server_status(self):
"""Gets the status of the server.
Returns:
The status of the server.
"""
pass
def _connect_hooks(self):
"""Creates hooks into all of Plover's events."""
if not self._engine:
plover.log.error(f'[stenogotchi_link] {ERROR_MISSING_ENGINE}')
raise AssertionError(ERROR_MISSING_ENGINE)
for hook in self._engine.HOOKS:
callback = getattr(self, f'_on_{hook}')
self._engine.hook_connect(hook, callback)
def _disconnect_hooks(self):
"""Removes hooks from all of Plover's events."""
if not self._engine:
raise AssertionError(ERROR_MISSING_ENGINE)
for hook in self._engine.HOOKS:
callback = getattr(self, f'_on_{hook}')
self._engine.hook_disconnect(hook, callback)
def _on_stroked(self, stroke: Stroke):
""" Broadcasts when a new stroke is performed. """
pass
def _on_translated(self, old: List[_Action], new: List[_Action]):
"""Broadcasts when a new translation occurs.
Args:
old: A list of the previous actions for the current translation.
new: A list of the new actions for the current translation.
"""
# Send to WPM meter if we have one
if self._wpm_meter:
self._wpm_meter.on_translation(old, new)
# Send to Strokes meter if we have one
if self._strokes_meter:
self._strokes_meter.on_translation(old, new)
# print(self._wpm_meter.get_stats())
# print(self._strokes_meter.get_stats())
def _on_machine_state_changed(self, machine_type: str, machine_state: str):
"""Broadcasts when the active machine state changes.
Args:
machine_type: The name of the active machine.
machine_state: The new machine state. This should be one of the
state constants listed in plover.machine.base.
"""
self._stenogotchiclient.plover_machine_state("machine_type: " + machine_type + " machine_state: " + machine_state)
def _on_output_changed(self, enabled: bool):
"""Broadcasts when the state of output changes.
Args:
enabled: If the output is now enabled or not.
"""
data = {'output_changed': enabled}
plover.log.debug(f'[stenogotchi_link] _on_output_changed data: {data}')
self._stenogotchiclient.plover_output_enabled(enabled)
def _on_config_changed(self, config_update: Config):
"""Broadcasts when the configuration changes.
Args:
config_update: An object containing the full configuration or a
part of the configuration that was updated.
"""
config_json = jsonpickle.encode(config_update, unpicklable=False)
data = {'config_changed': json.loads(config_json)}
plover.log.debug(f'[stenogotchi_link] _on_config_changed data: {data}')
def _on_dictionaries_loaded(self, dictionaries: StenoDictionaryCollection):
"""Broadcasts when all of the dictionaries get loaded.
Args:
dictionaries: A collection of the dictionaries that loaded.
"""
self._stenogotchiclient.plover_is_ready(True)
def _on_send_string(self, text: str):
"""Broadcasts when a new string is output.
Args:
text: The string that was output.
"""
if self._output_to_stenogotchi:
self._stenogotchiclient.send_string(text)
else:
self._btclient.send_string(text)
def _on_send_backspaces(self, count: int):
"""Broadcasts when backspaces are output.
Args:
count: The number of backspaces that were output.
"""
if self._output_to_stenogotchi:
self._stenogotchiclient.send_backspaces(count)
else:
self._btclient.send_backspaces(count)
def _on_send_key_combination(self, combination: str):
"""Broadcasts when a key combination is output.
Args:
combination: A string representing a sequence of key combinations.
Keys are represented by their names based on the OS-specific
keyboard implementations in plover.oslayer.
"""
if self._output_to_stenogotchi:
self._stenogotchiclient.send_key_combination(combination)
else:
self._btclient.send_key_combination(combination)
def _on_add_translation(self):
"""Broadcasts when the add translation tool is opened via a command."""
data = {'add_translation': True}
plover.log.debug(f'[stenogotchi_link] _on_add_translation data: {data}')
def _on_focus(self):
"""Broadcasts when the main window is focused via a command."""
data = {'focus': True}
plover.log.debug(f'[stenogotchi_link] _on_focus data: {data}')
def _on_configure(self):
"""Broadcasts when the configuration tool is opened via a command."""
data = {'configure': True}
plover.log.debug(f'[stenogotchi_link] _on_configure data: {data}')
def _on_lookup(self):
"""Broadcasts when the lookup tool is opened via a command."""
data = {'lookup': True}
plover.log.debug(f'[stenogotchi_link] _on_lookup data: {data}')
def _on_suggestions(self):
"""Broadcasts when the lookup tool is opened via a command."""
data = {'suggestions': True}
plover.log.debug(f'[stenogotchi_link] _on_lookup data: {data}')
def _on_quit(self):
"""Broadcasts when the application is terminated.
Can be either a full quit or a restart.
"""
data = {'quit': True}
plover.log.debug(f'[stenogotchi_link] _on_quit data: {data}')
self._stenogotchiclient.plover_is_running(False)
if __name__ == '__main__':
print("Please enable and run as Plover plugin")
================================================
FILE: plover_plugin/stenogotchi_link/wpm.py
================================================
#!/usr/bin/env python3
#
# Based on: https://github.com/arxanas/plover_wpm_meter.git, modified to remove QT dependency.
#
# This is a plugin for Plover to display your typing speed as you type, in either or both of words per minute or strokes per minute:
## Words per minute: Shows the rate at which you stroked words in the last 10 and 60 seconds.
## Strokes per minute: Shows how efficiently you stroked words in the last 10 and 60 seconds. Lower values are more efficient, e.g. a value of 1 means that on average it took you one stroke to write out a word.
#
# A word is defined in one of three ways:
## NCRA: The National Court Reporters Association defines a “word” as 1.4 syllables. This is the measure used for official NCRA testing material.
## Traditional: The traditional metric for “word” in the context of keyboarding is defined to be 5 characters per word, including spaces. This is compatible with the notion of “word” in many typing speed utilities.
## Spaces: A word is a whitespace-separated sequence of characters. This metric of course doesn’t take into account the fact that some words are longer than others, both in length and syllables.
import time
from threading import Timer
from textstat.textstat import textstat
from plover.formatting import OutputHelper
class RepeatTimer(Timer):
"""
Perpetually repeating timer implementation of threading.Timer
"""
def run(self):
while not self.finished.wait(self.interval):
self.function(*self.args, **self.kwargs)
class CaptureOutput(object):
def __init__(self, chars):
self.chars = chars
def send_backspaces(self, n):
del self.chars[-n:]
def send_string(self, s):
self.chars += _timestamp_items(s)
def send_key_combination(self, c):
pass
def send_engine_command(self, c):
pass
class BaseMeter():
def __init__(self, timeout=60):
# Set timer to calculate wpm/strokes stats each second
self._timer = RepeatTimer(1, self.on_timer)
self._timer.start()
# Set timer to publish wpm/strokes stats each minute
self._event_timer = RepeatTimer(timeout, self.trigger_event_update)
self._event_timer.start()
self.chars = []
def on_translation(self, old, new):
output = CaptureOutput(self.chars)
output_helper = OutputHelper(output, False, False)
output_helper.render(None, old, new)
def on_timer(self):
raise NotImplementedError()
def trigger_event_update(self):
raise NotImplementedError()
def quit(self):
self._timer.cancel()
self._event_timer.cancel()
class PloverWpmMeter(BaseMeter):
def __init__(self, stenogotchi_link, wpm_method='ncra', timeout=60):
super().__init__(timeout)
self._stenogotchi_link = stenogotchi_link
self.strokes = []
self.wpm_methods = {
'ncra': False, # NCRA (by syllables)
'traditional': False, # Traditional (by characters)
'spaces': False, # Spaces (by whitespace)
}
self._timeouts = {
"wpm10": 10,
"wpm_user": timeout,
}
self.set_wpm_method(wpm_method)
self.wpm_stats = {}
def set_wpm_method(self, method):
self.wpm_methods = dict.fromkeys(self.wpm_methods, False)
self.wpm_methods[method] = True
def get_wpm_method(self):
for method, enabled in self.wpm_methods.items():
if enabled:
return method
def get_stats(self):
return self.wpm_stats
def on_timer(self):
max_timeout = max(self._timeouts.values())
self.chars = _filter_old_items(self.chars, max_timeout)
for name, timeout in self._timeouts.items():
chars = _filter_old_items(self.chars, timeout)
wpm = _wpm_of_chars(chars, method=self.get_wpm_method())
self.wpm_stats[name] = str(wpm)
def trigger_event_update(self):
self._stenogotchi_link._on_wpm_meter_update_wpm(stats=self.wpm_stats)
class PloverStrokesMeter(BaseMeter):
def __init__(self, stenogotchi_link, strokes_method='ncra', timeout=60):
super().__init__(timeout)
self._stenogotchi_link = stenogotchi_link
self.actions = []
self.strokes_methods = {
'ncra': False, # NCRA (by syllables)
'traditional': False, # Traditional (by characters)
'spaces': False, # Spaces (by whitespace)
}
self._timeouts = {
"strokes10": 10,
"strokes_user": timeout,
}
self.set_strokes_method(strokes_method)
self.strokes_stats = {}
# By default, the QLCDNumbers will just display "0", without a decimal
# point, on initial render. Render them ourselves so that we don't
# switch from "0" to "0.00" after a second.
self.on_timer()
def set_strokes_method(self, method):
self.strokes_methods = dict.fromkeys(self.strokes_methods, False)
self.strokes_methods[method] = True
def get_strokes_method(self):
for method, enabled in self.strokes_methods.items():
if enabled:
return method
def get_stats(self):
return self.strokes_stats
def on_translation(self, old, new):
super().on_translation(old, new)
if len(old) > 0:
self.actions = self.actions[:-len(old)]
self.actions += _timestamp_items(new)
def on_timer(self):
max_timeout = max(self._timeouts.values())
self.chars = _filter_old_items(self.chars, max_timeout)
self.actions = _filter_old_items(self.actions, max_timeout)
for name, timeout in self._timeouts.items():
chars = _filter_old_items(self.chars, timeout)
num_strokes = len(_filter_old_items(self.actions, timeout))
strokes_per_word = _spw_of_chars(
num_strokes,
chars,
method=self.get_strokes_method()
)
self.strokes_stats[name] = str("{:0.2f}".format(strokes_per_word))
def trigger_event_update(self):
self._stenogotchi_link._on_wpm_meter_update_strokes(stats=self.strokes_stats)
def _timestamp_items(items):
current_time = time.time()
return [(i, current_time) for i in items]
def _filter_old_items(items, timeout):
current_time = time.time()
return [(i, t) for i, t in items
if (current_time - t) <= timeout]
def _words_in_chars(chars, method):
text = "".join(c for c, _ in chars)
if method == "ncra":
# The NCRA defines a "word" to be 1.4 syllables, which is the average
# number of syllables per English word.
syllables_per_word = 1.4
# For some reason, textstat returns syllable counts such as a
# one-syllable word like "the" being 0.9 syllables.
syllables_in_text = textstat.syllable_count(text) / 0.9
return syllables_in_text * (1 / syllables_per_word)
elif method == "traditional":
# Formal definition; see https://en.wikipedia.org/wiki/Words_per_minute
return len(text) / 5
elif method == "spaces":
return len([i for i in text.split() if i])
else:
assert False, "bad wpm method: " + method
def _time_interval_of_chars(chars):
start_time = min(t for _, t in chars)
current_time = time.time()
time_interval = current_time - start_time
time_interval = max(1, time_interval)
return time_interval
def _wpm_of_chars(chars, method):
num_words = _words_in_chars(chars, method)
if not num_words:
return 0
time_interval = _time_interval_of_chars(chars)
num_minutes = time_interval / 60
num_words_per_minute = num_words / num_minutes
return int(round(num_words_per_minute))
def _spw_of_chars(num_strokes, chars, method):
num_words = _words_in_chars(chars, method)
if not num_words:
return 0
return num_strokes / num_words
================================================
FILE: requirements.txt
================================================
PyGObject == 3.30.4
dbus_python == 1.2.16
evdev == 1.4.0
RPi.GPIO == 0.7.0
file_read_backwards == 2.0.0
toml == 0.10.2
spidev == 3.5
Flask == 1.1.2
Flask_Cors == 3.0.10
Flask_WTF == 0.14.3
requests == 2.25.1
Pillow >= 8.3.2
plover >= 4.0.0.dev10
================================================
FILE: stenogotchi/__init__.py
================================================
import os
import logging
import time
import re
import subprocess
from stenogotchi._version import __version__
_name = None
config = None
def set_name(new_name):
if new_name is None:
return
new_name = new_name.strip()
if new_name == '':
return
if not re.match(r'^[a-zA-Z0-9\-]{2,25}$', new_name):
logging.warning("name '%s' is invalid: min length is 2, max length 25, only a-zA-Z0-9- allowed", new_name)
return
current = name()
if new_name != current:
global _name
logging.info("setting unit hostname '%s' -> '%s'", current, new_name)
with open('/etc/hostname', 'wt') as fp:
fp.write(new_name)
with open('/etc/hosts', 'rt') as fp:
prev = fp.read()
logging.debug("old hosts:\n%s\n", prev)
with open('/etc/hosts', 'wt') as fp:
patched = prev.replace(current, new_name, -1)
logging.debug("new hosts:\n%s\n", patched)
fp.write(patched)
os.system("hostname '%s'" % new_name)
reboot()
def name():
global _name
if _name is None:
with open('/etc/hostname', 'rt') as fp:
_name = fp.read().strip()
return _name
def uptime():
with open('/proc/uptime') as fp:
return int(fp.read().split('.')[0])
def mem_usage():
with open('/proc/meminfo') as fp:
for line in fp:
line = line.strip()
if line.startswith("MemTotal:"):
kb_mem_total = int(line.split()[1])
if line.startswith("MemFree:"):
kb_mem_free = int(line.split()[1])
if line.startswith("Buffers:"):
kb_main_buffers = int(line.split()[1])
if line.startswith("Cached:"):
kb_main_cached = int(line.split()[1])
kb_mem_used = kb_mem_total - kb_mem_free - kb_main_cached - kb_main_buffers
return round(kb_mem_used / kb_mem_total, 1)
return 0
def _cpu_stat():
"""
Returns the splitted first line of the /proc/stat file
"""
with open('/proc/stat', 'rt') as fp:
return list(map(int,fp.readline().split()[1:]))
def cpu_load(s=0.1):
"""
Returns the average cpuload over a 's'-seconds period
"""
parts0 = _cpu_stat()
time.sleep(s)
parts1 = _cpu_stat()
parts_diff = [p1 - p0 for (p0, p1) in zip(parts0, parts1)]
user, nice, sys, idle, iowait, irq, softirq, steal, _guest, _guest_nice = parts_diff
idle_sum = idle + iowait
non_idle_sum = user + nice + sys + irq + softirq + steal
total = idle_sum + non_idle_sum
return non_idle_sum / total
def temperature(celsius=True):
with open('/sys/class/thermal/thermal_zone0/temp', 'rt') as fp:
temp = int(fp.read().strip())
c = int(temp / 1000)
return c if celsius else ((c * (9 / 5)) + 32)
def get_wifi_status():
try:
parts = os.popen("sudo ip -br addr show wlan0").read().split()
state = parts[1]
ip = parts[2].split('/')[0]
return state, ip
except:
return None, None
def get_wifi_ssid():
try:
ssid = os.popen("sudo iwgetid -r").read().split('\n')[0]
return ssid
except:
return ""
def set_wifi_onoff():
wifi_updown = get_wifi_status()[0]
if wifi_updown == 'UP':
# switch wifi off
subprocess.call(['sudo', 'ip', 'link', 'set', 'dev', 'wlan0', 'down'])
subprocess.call(['sudo', 'dhclient', '-r', 'wlan0'])
subprocess.call(['sudo', 'rfkill', 'block', '0'])
return False
else:
# switch wifi on
subprocess.call(['sudo', 'rfkill', 'unblock', '0'])
subprocess.call(['sudo', 'dhclient', 'wlan0'])
subprocess.call(['sudo', 'ip', 'link', 'set', 'dev', 'wlan0', 'up'])
return True
def shutdown():
logging.warning("shutting down ...")
from stenogotchi.ui import view
if view.ROOT:
view.ROOT.on_shutdown()
# give it some time to refresh the ui
time.sleep(5)
if view.ROOT._config['ui']['display']['enabled']:
if view.ROOT._config['ui']['display']['clear_at_shutdown']:
view.ROOT._agent._view.init_display()
view.ROOT._agent._view.clear()
# give it some time to clear the ui
time.sleep(5)
logging.warning("syncing...")
from stenogotchi import fs
for m in fs.mounts:
m.sync()
os.system("sync")
os.system("halt")
def restart(mode):
logging.warning("restarting in %s mode ...", mode)
if mode == 'AUTO':
os.system("touch /root/.stenogotchi-auto")
else:
os.system("touch /root/.stenogotchi-manual")
#os.system("service stenogotchi restart")
def reboot(mode=None):
if mode is not None:
mode = mode.upper()
logging.warning("rebooting in %s mode ...", mode)
else:
logging.warning("rebooting ...")
from stenogotchi.ui import view
if view.ROOT:
view.ROOT.on_rebooting()
# give it some time to refresh the ui
time.sleep(10)
if mode == 'AUTO':
os.system("touch /root/.stenogotchi-auto")
elif mode == 'MANU':
os.system("touch /root/.stenogotchi-manual")
logging.warning("syncing...")
from stenogotchi import fs
for m in fs.mounts:
m.sync()
os.system("sync")
os.system("shutdown -r now")
================================================
FILE: stenogotchi/_version.py
================================================
__version__ = '0.1.0'
================================================
FILE: stenogotchi/agent.py
================================================
import time
import json
import os
import re
import logging
import asyncio
import _thread
import stenogotchi
import stenogotchi.utils as utils
import stenogotchi.plugins as plugins
from stenogotchi.ui.web.server import Server
from stenogotchi.automata import Automata
#from stenogotchi.log import LastSession
RECOVERY_DATA_FILE = '/root/.stenogotchi-recovery'
class Agent(Automata):
def __init__(self, view, config):
Automata.__init__(self, config, view)
self._started_at = time.time()
self._view = view
self._view.set_agent(self)
self._web_ui = None
self._wifi_connected = None
self._history = {}
#self.last_session = LastSession(self._config)
self.mode = 'auto'
logging.info("%s (v%s)", stenogotchi.name(), stenogotchi.__version__)
for _, plugin in plugins.loaded.items():
logging.debug("plugin '%s' v%s", plugin.__class__.__name__, plugin.__version__)
def config(self):
return self._config
def view(self):
return self._view
def start(self):
self.set_starting()
# self.start_event_polling()
# print initial stats
self.start_session_fetcher()
self.set_ready()
def _update_uptime(self):
secs = stenogotchi.uptime()
self._view.set('uptime', utils.secs_to_hhmmss(secs))
def _update_wifi(self):
status, ip = stenogotchi.get_wifi_status()
if status == 'UP':
if self._wifi_connected:
return
ssid = stenogotchi.get_wifi_ssid()
if ssid and ip:
self._wifi_connected = True
# Create web-ui only once a wifi connection is established
if not self._web_ui:
self._web_ui = Server(self, self._config['ui'])
else:
if not ssid:
ssid = "[Searching]"
ip = "the ghost"
if not ip:
ip = "the ghost"
self._wifi_connected = False
self.set_wifi_connected(ssid, ip)
elif status == 'DOWN' or not status:
if self._wifi_connected == False:
return
self._wifi_connected = False
self.set_wifi_disconnected('[OFF]')
def _reboot(self):
self.set_rebooting()
self._save_recovery_data()
stenogotchi.reboot()
def _save_recovery_data(self):
logging.warning("writing recovery data to %s ...", RECOVERY_DATA_FILE)
with open(RECOVERY_DATA_FILE, 'w') as fp:
data = {
'started_at': self._started_at,
'history': self._history,
}
json.dump(data, fp)
def _load_recovery_data(self, delete=True, no_exceptions=True):
try:
with open(RECOVERY_DATA_FILE, 'rt') as fp:
data = json.load(fp)
logging.info("found recovery data: %s", data)
self._started_at = data['started_at']
self._history = data['history']
if delete:
logging.info("deleting %s", RECOVERY_DATA_FILE)
os.unlink(RECOVERY_DATA_FILE)
except:
if not no_exceptions:
raise
def start_session_fetcher(self):
_thread.start_new_thread(self._fetch_stats, ())
async def plover_status_update(self, msg):
pass
def _fetch_stats(self):
while True:
#s = self.session() # this is just the request object for connection to bettercap -> to be used for fetching plover stats instead
self._update_uptime()
self._update_wifi()
time.sleep(30)
async def _on_event(self, msg): # no bettercap to produce events for us, to be used for plover events
pass
def _event_poller(self, loop):
self._load_recovery_data()
#while True:
def start_event_polling(self):
# start a thread and pass in the mainloop
_thread.start_new_thread(self._event_poller, (asyncio.get_event_loop(),))
================================================
FILE: stenogotchi/automata.py
================================================
import logging
import stenogotchi.plugins as plugins
# basic mood system
class Automata(object):
def __init__(self, config, view):
self._config = config
self._view = view
def set_starting(self):
self._view.on_starting()
def set_ready(self):
plugins.on('ready', self)
def in_good_mood(self):
pass
# triggered when it's a sad/bad day but you have good friends around ^_^
def set_grateful(self):
self._view.on_grateful()
plugins.on('grateful', self)
def set_lonely(self):
self._view.on_lonely()
plugins.on('lonely', self)
def set_bored(self):
self._view.on_bored()
plugins.on('bored', self)
def set_sad(self):
self._view.on_sad()
plugins.on('sad', self)
def set_angry(self):
self._view.on_angry()
plugins.on('angry', self)
def set_excited(self):
self._view.on_excited()
plugins.on('excited', self)
def set_processing(self):
self._view.on_processing()
plugins.on('processing', self)
def set_rebooting(self):
self._view.on_rebooting()
plugins.on('rebooting', self)
def wait_for(self, t, sleeping=True):
self._view.wait(t, sleeping)
def set_plover_boot(self):
self._view.on_plover_boot()
plugins.on('plover_boot', self)
def set_plover_ready(self):
self._view.on_plover_ready()
plugins.on('plover_ready', self)
def set_plover_quit(self):
self._view.on_plover_quit()
plugins.on('plover_quit', self)
def set_bt_connected(self, bthost_name):
self._view.set('bthost', bthost_name)
self._view.on_bt_connected(bthost_name)
plugins.on('bt_connected', self, bthost_name)
def set_bt_disconnected(self):
self._view.set('bthost', "")
self._view.on_bt_disconnected()
plugins.on('bt_disconnected', self)
def set_wifi_connected(self, ssid, ip):
self._view.set('wifi', ssid)
self._view.on_wifi_connected(ssid, ip)
plugins.on('wifi_connected', self, ssid, ip)
def set_wifi_disconnected(self, ssid=''):
self._view.set('wifi', ssid)
self._view.on_wifi_disconnected()
plugins.on('wifi_disconnected', self)
def set_wpm(self, wpm, wpm_top):
stats = '{:3s} {:5s}'.format(str(wpm), f'({wpm_top})')
self._view.on_wpm(stats)
plugins.on('wpm_set', self)
def set_strokes(self, stats):
self._view.on_strokes(stats)
plugins.on('strokes_set', self)
def set_wpm_record(self, wpm_top):
self._view.on_wpm_record(wpm_top)
plugins.on('wpm_record', self)
self.set_wpm(wpm_top, wpm_top) # Since we set a new record wpm and wpm_top will be equal
def set_on_dict_lookup_done(self):
self._view.on_dict_lookup_done()
plugins.on('dict_lookup_done', self)
================================================
FILE: stenogotchi/defaults.toml
================================================
# This is a TOML document
# This is the default config template. To change the default values, please save your modifications to /etc/stenogotchi/config.toml
# Main
main.name = "Stenogotchi"
main.lang = "en"
main.confd = "/etc/stenogotchi/conf.d/"
main.custom_plugins = ""
# Filesystem
fs.memory.enabled = false
fs.memory.mounts.log.enabled = false
fs.memory.mounts.log.mount = "/var/log"
fs.memory.mounts.log.size = "50M"
fs.memory.mounts.log.sync = 60
fs.memory.mounts.log.zram = true
fs.memory.mounts.log.rsync = true
fs.memory.mounts.data.enabled = false
fs.memory.mounts.data.mount = "/var/tmp/stenogotchi"
fs.memory.mounts.data.size = "10M"
fs.memory.mounts.data.sync = 3600
fs.memory.mounts.data.zram = false
fs.memory.mounts.data.rsync = true
# UI
# The lifespan of an eINK display depends on the cumulative number of refreshes. To preserve your display
# over time it is recommended to keep ui.fps value 0.0 so that refreshes only take place upon major updates.
# Uptime, battery status, the blinking cursor and similar will not trigger a refresh
ui.fps = 0.0
ui.font.name = "DejaVuSansMono"
ui.font.size_offset = 0 # added to the font size
ui.faces.look_r = "( ⚆_⚆)"
ui.faces.look_l = "(☉_☉ )"
ui.faces.look_r_happy = "( ◕‿◕)"
ui.faces.look_l_happy = "(◕‿◕ )"
ui.faces.sleep = "(⇀‿‿↼)"
ui.faces.sleep2 = "(≖‿‿≖)"
ui.faces.awake = "(◕‿‿◕)"
ui.faces.bored = "(-__-)"
ui.faces.intense = "(°▃▃°)"
ui.faces.cool = "(⌐■_■)"
ui.faces.happy = "(•‿‿•)"
ui.faces.excited = "(ᵔ◡◡ᵔ)"
ui.faces.grateful = "(^‿‿^)"
ui.faces.motivated = "(☼‿‿☼)"
ui.faces.demotivated = "(≖__≖)"
ui.faces.smart = "(✜‿‿✜)"
ui.faces.lonely = "(ب__ب)"
ui.faces.sad = "(╥☁╥ )"
ui.faces.angry = "(-_-')"
ui.faces.friend = "(♥‿‿♥)"
ui.faces.broken = "(☓‿‿☓)"
ui.faces.debug = "(#__#)"
ui.faces.process_1 = "(1__0)"
ui.faces.process_2 = "(1__1)"
ui.faces.process_3 = "(0__1)"
ui.faces.process_4 = "(0__0)"
ui.display.enabled = true
ui.display.rotation = 0
ui.display.type = "waveshare_2"
ui.display.color = "black"
ui.display.clear_at_shutdown = true
# The web UI provides an alternative way of interacting with the device to an eINK display and ButtonSHIM module. Enabling the web UI will also load the buttonshim plugin.
ui.web.enabled = false
ui.web.address = "192.168.1.100"
ui.web.username = "changeme"
ui.web.password = "changeme"
ui.web.origin = ""
ui.web.port = 80
ui.web.on_frame = ""
# Plugins
# Populate bt_autoconnect_mac with the MAC addresses of devices to attempt auto connecting to at boot. Multiple comma-separated devices in order of priority can be given.
# After exhausting the options, the device will fall back to listening for incoming connection and pairing attempts.
## Example:
## main.plugins.plover_link.bt_autoconnect_mac = "00:DE:AD:BE:EF:00, 11:DE:AD:BE:EF:11"
main.plugins.plover_link.enabled = true
main.plugins.plover_link.bt_autoconnect_mac = ""
main.plugins.plover_link.bt_device_name = "Stenogotchi"
# For WPM readings a word is defined in one of three ways:
## NCRA: The National Court Reporters Association defines a “word” as 1.4 syllables. This is the measure used for official NCRA testing material.
## Traditional: The traditional metric for “word” in the context of keyboarding is defined to be 5 characters per word, including spaces. This is compatible with the notion of “word” in many typing speed utilities.
## Spaces: A word is a whitespace-separated sequence of characters. This metric of course doesn’t take into account the fact that some words are longer than others, both in length and syllables.
# Specify either "ncra", "traditional" or "spaces" as preferred method using main.plugins.plover_link.wpm_method
# Specify in seconds moving time window for which wpm is calculated and updated using main.plugins.plover_link.wpm_timeout
main.plugins.plover_link.wpm_method = "traditional"
main.plugins.plover_link.wpm_timeout = "60"
# Plugin evdevkb is responsible for direct keyboard capturing, bypassing Plover. Input mode toggleable when enabled.
main.plugins.evdevkb.enabled = true
# Plugin dict_lookup handles dictionary lookup from all enabled Plover dictionaries.
main.plugins.dict_lookup.enabled = true
# Plugin upslite provides battery level, charging indicator (+) and auto-shutdown at specified battery level when a UPS-Lite v1.2 module is connected.
main.plugins.upslite.enabled = false
main.plugins.upslite.shutdown_level = 5
# Plugin buttonshim enables a way to interact with the device using the five physical buttons on the ButtonSHIM hardware module or through the web UI. Enabling the web UI will automatically load the buttonshim plugin.
# Each button has a predefined long-press (1s) action. The short-press action can be freely configured to any terminal command.
main.plugins.buttonshim.enabled = false
main.plugins.buttonshim.buttons.A.command = ""
main.plugins.buttonshim.buttons.A.blink.enabled = false
main.plugins.buttonshim.buttons.A.blink.red = 0
main.plugins.buttonshim.buttons.A.blink.green = 0
main.plugins.buttonshim.buttons.A.blink.blue = 0
main.plugins.buttonshim.buttons.A.blink.on_time = 0
main.plugins.buttonshim.buttons.A.blink.off_time = 0
main.plugins.buttonshim.buttons.A.blink.blink_times = 0
main.plugins.buttonshim.buttons.B.command = ""
main.plugins.buttonshim.buttons.B.blink.enabled = false
main.plugins.buttonshim.buttons.B.blink.red = 0
main.plugins.buttonshim.buttons.B.blink.green = 0
main.plugins.buttonshim.buttons.B.blink.blue = 0
main.plugins.buttonshim.buttons.B.blink.on_time = 0
main.plugins.buttonshim.buttons.B.blink.off_time = 0
main.plugins.buttonshim.buttons.B.blink.blink_times = 0
main.plugins.buttonshim.buttons.C.command = ""
main.plugins.buttonshim.buttons.C.blink.enabled = false
main.plugins.buttonshim.buttons.C.blink.red = 0
main.plugins.buttonshim.buttons.C.blink.green = 0
main.plugins.buttonshim.buttons.C.blink.blue = 0
main.plugins.buttonshim.buttons.C.blink.on_time = 0
main.plugins.buttonshim.buttons.C.blink.off_time = 0
main.plugins.buttonshim.buttons.C.blink.blink_times = 0
main.plugins.buttonshim.buttons.D.command = ""
main.plugins.buttonshim.buttons.D.blink.enabled = false
main.plugins.buttonshim.buttons.D.blink.red = 0
main.plugins.buttonshim.buttons.D.blink.green = 0
main.plugins.buttonshim.buttons.D.blink.blue = 0
main.plugins.buttonshim.buttons.D.blink.on_time = 0
main.plugins.buttonshim.buttons.D.blink.off_time = 0
main.plugins.buttonshim.buttons.D.blink.blink_times = 0
main.plugins.buttonshim.buttons.E.command = ""
main.plugins.buttonshim.buttons.E.blink.enabled = false
main.plugins.buttonshim.buttons.E.blink.red = 0
main.plugins.buttonshim.buttons.E.blink.green = 0
main.plugins.buttonshim.buttons.E.blink.blue = 0
main.plugins.buttonshim.buttons.E.blink.on_time = 0
main.plugins.buttonshim.buttons.E.blink.off_time = 0
main.plugins.buttonshim.buttons.E.blink.blink_times = 0
# Plugin led provides an alternative way to be alerted to events through the built-in LED on the rpi0w without the need of a display.
main.plugins.led.enabled = false
main.plugins.led.led = 0
main.plugins.led.delay = 200
main.plugins.led.patterns.loaded = ""
main.plugins.led.patterns.updating = ""
main.plugins.led.patterns.ready = ""
main.plugins.led.patterns.grateful = ""
main.plugins.led.patterns.lonely = ""
main.plugins.led.patterns.bored = ""
main.plugins.led.patterns.sad = ""
main.plugins.led.patterns.angry = ""
main.plugins.led.patterns.excited = ""
main.plugins.led.patterns.rebooting = "oo oo oo oo oo"
main.plugins.led.patterns.wait = ""
main.plugins.led.patterns.sleep = ""
main.plugins.led.patterns.epoch = ""
main.plugins.led.patterns.bt_connected = "oo oo oo oo oo"
main.plugins.led.patterns.bt_disconnected = "oo oo oo oo oo"
main.plugins.led.patterns.plover_boot = ""
main.plugins.led.patterns.plover_ready = "oo oo oo oo oo"
main.plugins.led.patterns.plover_quit = "oo oo oo oo oo"
main.plugins.led.patterns.wifi_connected = "oo oo oo oo oo"
main.plugins.led.patterns.wifi_disconnected = "oo oo oo oo oo"
main.plugins.led.patterns.wpm_set = ""
main.plugins.led.patterns.strokes_set = ""
# Plugin memtemp displays CPU temperature and RAM/CPU usage.
main.plugins.memtemp.enabled = false
main.plugins.memtemp.scale = "celsius"
main.plugins.memtemp.orientation = "horizontal"
# Plugin logtail gives access to the Stenogotchi log through the web UI.
main.plugins.logtail.enabled = false
main.plugins.logtail.max-lines = 10000
main.plugins.session-stats.enabled = false
main.plugins.session-stats.save_directory = "/var/tmp/stenogotchi/sessions/"
# Log
main.log.path = "/var/log/stenogotchi.log"
main.log.rotation.enabled = true
main.log.rotation.size = "10M"
================================================
FILE: stenogotchi/fs/__init__.py
================================================
import os
import re
import tempfile
import contextlib
import shutil
import _thread
import logging
from time import sleep
from distutils.dir_util import copy_tree
mounts = list()
@contextlib.contextmanager
def ensure_write(filename, mode='w'):
path = os.path.dirname(filename)
fd, tmp = tempfile.mkstemp(dir=path)
with os.fdopen(fd, mode) as f:
yield f
f.flush()
os.fsync(f.fileno())
os.replace(tmp, filename)
def size_of(path):
"""
Calculate the sum of all the files in path
"""
total = 0
for root, _, files in os.walk(path):
for f in files:
total += os.path.getsize(os.path.join(root, f))
return total
def is_mountpoint(path):
"""
Checks if path is mountpoint
"""
return os.system(f"mountpoint -q {path}") == 0
def setup_mounts(config):
"""
Sets up all the configured mountpoints
"""
global mounts
fs_cfg = config['fs']['memory']
if not fs_cfg['enabled']:
return
for name, options in fs_cfg['mounts'].items():
if not options['enabled']:
continue
logging.debug("[FS] Trying to setup mount %s (%s)", name, options['mount'])
size,unit = re.match(r"(\d+)([a-zA-Z]+)", options['size']).groups()
target = os.path.join('/run/stenogotchi/disk/', os.path.basename(options['mount']))
is_mounted = is_mountpoint(target)
logging.debug("[FS] %s is %s mounted", options['mount'],
"already" if is_mounted else "not yet")
m = MemoryFS(
options['mount'],
target,
size=options['size'],
zram=options['zram'],
zram_disk_size=f"{int(size)*2}{unit}",
rsync=options['rsync'])
if not is_mounted:
if not m.mount():
logging.error(f"Error while mounting {m.mountpoint}")
continue
if not m.sync(to_ram=True):
logging.error(f"Error while syncing to {m.mountpoint}")
m.umount()
continue
interval = int(options['sync'])
if interval:
logging.debug("[FS] Starting thread to sync %s (interval: %d)",
options['mount'], interval)
_thread.start_new_thread(m.daemonize, (interval,))
else:
logging.debug("[FS] Not syncing %s, because interval is 0",
options['mount'])
mounts.append(m)
class MemoryFS:
@staticmethod
def zram_install():
if not os.path.exists("/sys/class/zram-control"):
logging.debug("[FS] Installing zram")
return os.system("modprobe zram") == 0
return True
@staticmethod
def zram_dev():
logging.debug("[FS] Adding zram device")
return open("/sys/class/zram-control/hot_add", "rt").read().strip("\n")
def __init__(self, mount, disk, size="40M",
zram=True, zram_alg="lz4", zram_disk_size="100M",
zram_fs_type="ext4", rsync=True):
self.mountpoint = mount
self.disk = disk
self.size = size
self.zram = zram
self.zram_alg = zram_alg
self.zram_disk_size = zram_disk_size
self.zram_fs_type = zram_fs_type
self.zdev = None
self.rsync = True
self._setup()
def _setup(self):
if self.zram and MemoryFS.zram_install():
# setup zram
self.zdev = MemoryFS.zram_dev()
open(f"/sys/block/zram{self.zdev}/comp_algorithm", "wt").write(self.zram_alg)
open(f"/sys/block/zram{self.zdev}/disksize", "wt").write(self.zram_disk_size)
open(f"/sys/block/zram{self.zdev}/mem_limit", "wt").write(self.size)
logging.debug("[FS] Creating fs (type: %s)", self.zram_fs_type)
os.system(f"mke2fs -t {self.zram_fs_type} /dev/zram{self.zdev} >/dev/null 2>&1")
# ensure mountpoints exist
if not os.path.exists(self.disk):
logging.debug("[FS] Creating %s", self.disk)
os.makedirs(self.disk)
if not os.path.exists(self.mountpoint):
logging.debug("[FS] Creating %s", self.mountpoint)
os.makedirs(self.mountpoint)
def daemonize(self, interval=60):
logging.debug("[FS] Daemonized...")
while True:
self.sync()
sleep(interval)
def sync(self, to_ram=False):
source, dest = (self.disk, self.mountpoint) if to_ram else (self.mountpoint, self.disk)
needed, actually_free = size_of(source), shutil.disk_usage(dest)[2]
if actually_free >= needed:
logging.debug("[FS] Syncing %s -> %s", source,dest)
if self.rsync:
os.system(f"rsync -aXv --inplace --no-whole-file --delete-after {source}/ {dest}/ >/dev/null 2>&1")
else:
copy_tree(source, dest, preserve_symlinks=True)
os.system("sync")
return True
return False
def mount(self):
if os.system(f"mount --bind {self.mountpoint} {self.disk}"):
return False
if os.system(f"mount --make-private {self.disk}"):
return False
if self.zram and self.zdev is not None:
if os.system(f"mount -t {self.zram_fs_type} -o nosuid,noexec,nodev,user=stenogotchi /dev/zram{self.zdev} {self.mountpoint}/"):
return False
else:
if os.system(f"mount -t tmpfs -o nosuid,noexec,nodev,mode=0755,size={self.size} stenogotchi {self.mountpoint}/"):
return False
return True
def umount(self):
if os.system(f"umount -l {self.mountpoint}"):
return False
if os.system(f"umount -l {self.disk}"):
return False
return True
================================================
FILE: stenogotchi/log.py
================================================
import hashlib
import time
import re
import os
import logging
import shutil
import gzip
import warnings
from datetime import datetime
from stenogotchi.voice import Voice
from file_read_backwards import FileReadBackwards
LAST_SESSION_FILE = '/root/.stenogotchi-last-session'
class LastSession(object):
START_TOKEN = 'connecting to http'
def __init__(self, config):
self.config = config
self.voice = Voice(lang=config['main']['lang'])
self.path = config['main']['log']['path']
self.last_session = []
self.last_session_id = ''
self.last_saved_session_id = ''
self.duration = ''
self.duration_human = ''
def _get_last_saved_session_id(self):
saved = ''
try:
with open(LAST_SESSION_FILE, 'rt') as fp:
saved = fp.read().strip()
except:
saved = ''
return saved
def save_session_id(self):
with open(LAST_SESSION_FILE, 'w+t') as fp:
fp.write(self.last_session_id)
self.last_saved_session_id = self.last_session_id
def _parse_datetime(self, dt):
dt = dt.split('.')[0]
dt = dt.split(',')[0]
dt = datetime.strptime(dt.split('.')[0], '%Y-%m-%d %H:%M:%S')
return time.mktime(dt.timetuple())
def _parse_stats(self):
self.duration = ''
self.duration_human = ''
started_at = None
stopped_at = None
cache = {}
for line in self.last_session:
parts = line.split(']')
if len(parts) < 2:
continue
try:
line_timestamp = parts[0].strip('[')
line = ']'.join(parts[1:])
stopped_at = self._parse_datetime(line_timestamp)
if started_at is None:
started_at = stopped_at
except Exception as e:
logging.error("error parsing line '%s': %s" % (line, e))
if started_at is not None:
self.duration = stopped_at - started_at
mins, secs = divmod(self.duration, 60)
hours, mins = divmod(mins, 60)
else:
hours = mins = secs = 0
self.duration = '%02d:%02d:%02d' % (hours, mins, secs)
self.duration_human = []
if hours > 0:
self.duration_human.append('%d %s' % (hours, self.voice.hhmmss(hours, 'h')))
if mins > 0:
self.duration_human.append('%d %s' % (mins, self.voice.hhmmss(mins, 'm')))
if secs > 0:
self.duration_human.append('%d %s' % (secs, self.voice.hhmmss(secs, 's')))
self.duration_human = ', '.join(self.duration_human)
def parse(self, ui, skip=False):
if skip:
logging.debug("skipping parsing of the last session logs ...")
else:
logging.debug("reading last session logs ...")
ui.on_reading_logs()
lines = []
if os.path.exists(self.path):
with FileReadBackwards(self.path, encoding="utf-8") as fp:
for line in fp:
line = line.strip()
if line != "" and line[0] != '[':
continue
lines.append(line)
if LastSession.START_TOKEN in line:
break
lines_so_far = len(lines)
if lines_so_far % 100 == 0:
ui.on_reading_logs(lines_so_far)
lines.reverse()
if len(lines) == 0:
lines.append("Initial Session");
ui.on_reading_logs()
self.last_session = lines
self.last_session_id = hashlib.md5(lines[0].encode()).hexdigest()
self.last_saved_session_id = self._get_last_saved_session_id()
logging.debug("parsing last session logs (%d lines) ..." % len(lines))
self._parse_stats()
self.parsed = True
def is_new(self):
return self.last_session_id != self.last_saved_session_id
def setup_logging(args, config):
cfg = config['main']['log']
filename = cfg['path']
formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")
root = logging.getLogger()
root.setLevel(logging.DEBUG if args.debug else logging.INFO)
if filename:
# since python default log rotation might break session data in different files,
# we need to do log rotation ourselves
log_rotation(filename, cfg)
file_handler = logging.FileHandler(filename)
file_handler.setFormatter(formatter)
root.addHandler(file_handler)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
root.addHandler(console_handler)
def log_rotation(filename, cfg):
rotation = cfg['rotation']
if not rotation['enabled']:
return
elif not os.path.isfile(filename):
return
stats = os.stat(filename)
# specify a maximum size to rotate ( format is 10/10B, 10K, 10M 10G )
if rotation['size']:
max_size = parse_max_size(rotation['size'])
if stats.st_size >= max_size:
do_rotate(filename, stats, cfg)
else:
raise Exception("log rotation is enabled but log.rotation.size was not specified")
def parse_max_size(s):
parts = re.findall(r'(^\d+)([bBkKmMgG]?)', s)
if len(parts) != 1 or len(parts[0]) != 2:
raise Exception("can't parse %s as a max size" % s)
num, unit = parts[0]
num = int(num)
unit = unit.lower()
if unit == 'k':
return num * 1024
elif unit == 'm':
return num * 1024 * 1024
elif unit == 'g':
return num * 1024 * 1024 * 1024
else:
return num
def do_rotate(filename, stats, cfg):
base_path = os.path.dirname(filename)
name = os.path.splitext(os.path.basename(filename))[0]
archive_filename = os.path.join(base_path, "%s.gz" % name)
counter = 2
while os.path.exists(archive_filename):
archive_filename = os.path.join(base_path, "%s-%d.gz" % (name, counter))
counter += 1
log_filename = archive_filename.replace('gz', 'log')
print("%s is %d bytes big, rotating to %s ..." % (filename, stats.st_size, log_filename))
shutil.move(filename, log_filename)
print("compressing to %s ..." % archive_filename)
with open(log_filename, 'rb') as src:
with gzip.open(archive_filename, 'wb') as dst:
dst.writelines(src)
================================================
FILE: stenogotchi/plugins/__init__.py
================================================
import os
import glob
import _thread
import threading
import importlib, importlib.util
import logging
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
loaded = {}
database = {}
locks = {}
class Plugin:
@classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
global loaded, locks
plugin_name = cls.__module__.split('.')[0]
plugin_instance = cls()
logging.debug("loaded plugin %s as %s" % (plugin_name, plugin_instance))
loaded[plugin_name] = plugin_instance
for attr_name in plugin_instance.__dir__():
if attr_name.startswith('on_'):
cb = getattr(plugin_instance, attr_name, None)
if cb is not None and callable(cb):
locks["%s::%s" % (plugin_name, attr_name)] = threading.Lock()
def toggle_plugin(name, enable=True):
"""
Load or unload a plugin
returns True if changed, otherwise False
"""
import stenogotchi
from stenogotchi.ui import view
from stenogotchi.utils import save_config
global loaded, database
# log event
if enable:
logging.debug(f"enabled plugin '{name}' in config")
else:
logging.debug(f"disabled plugin '{name}' in config")
if stenogotchi.config:
if not name in stenogotchi.config['main']['plugins']:
stenogotchi.config['main']['plugins'][name] = dict()
stenogotchi.config['main']['plugins'][name]['enabled'] = enable
save_config(stenogotchi.config, '/etc/stenogotchi/config.toml')
if not enable and name in loaded:
if getattr(loaded[name], 'on_unload', None):
loaded[name].on_unload(view.ROOT)
del loaded[name]
return True
if enable and name in database and name not in loaded:
load_from_file(database[name])
if name in loaded and stenogotchi.config and name in stenogotchi.config['main']['plugins']:
loaded[name].options = stenogotchi.config['main']['plugins'][name]
one(name, 'loaded')
if stenogotchi.config:
one(name, 'config_changed', stenogotchi.config)
one(name, 'ui_setup', view.ROOT)
one(name, 'ready', view.ROOT._agent)
return True
return False
def on(event_name, *args, **kwargs):
for plugin_name in loaded.keys():
one(plugin_name, event_name, *args, **kwargs)
def locked_cb(lock_name, cb, *args, **kwargs):
global locks
if lock_name not in locks:
locks[lock_name] = threading.Lock()
with locks[lock_name]:
cb(*args, *kwargs)
def one(plugin_name, event_name, *args, **kwargs):
global loaded
if plugin_name in loaded:
plugin = loaded[plugin_name]
cb_name = 'on_%s' % event_name
callback = getattr(plugin, cb_name, None)
if callback is not None and callable(callback):
try:
lock_name = "%s::%s" % (plugin_name, cb_name)
locked_cb_args = (lock_name, callback, *args, *kwargs)
_thread.start_new_thread(locked_cb, locked_cb_args)
except Exception as e:
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
logging.error(e, exc_info=True)
def load_from_file(filename):
logging.debug("loading %s" % filename)
plugin_name = os.path.basename(filename.replace(".py", ""))
spec = importlib.util.spec_from_file_location(plugin_name, filename)
instance = importlib.util.module_from_spec(spec)
spec.loader.exec_module(instance)
return plugin_name, instance
def load_from_path(path, enabled=()):
global loaded, database
logging.debug("loading plugins from %s - enabled: %s" % (path, enabled))
for filename in glob.glob(os.path.join(path, "*.py")):
plugin_name = os.path.basename(filename.replace(".py", ""))
database[plugin_name] = filename
if plugin_name in enabled:
try:
load_from_file(filename)
except Exception as e:
logging.warning("error while loading %s: %s" % (filename, e))
logging.debug(e, exc_info=True)
return loaded
def load(config):
enabled = [name for name, options in config['main']['plugins'].items() if
'enabled' in options and options['enabled']]
# force enable buttonshim plugin when web ui is enabled
logging.debug(f"REMOVEME pre status : '{enabled}'")
if config['ui']['web']['enabled'] and 'buttonshim' not in enabled:
enabled.append('buttonshim')
logging.debug(f"REMOVEME post status : '{enabled}'")
# load default plugins
load_from_path(default_path, enabled=enabled)
# load custom ones
custom_path = config['main']['custom_plugins'] if 'custom_plugins' in config['main'] else None
if custom_path is not None:
load_from_path(custom_path, enabled=enabled)
# propagate options
for name, plugin in loaded.items():
plugin.options = config['main']['plugins'][name]
print("loaded plugin:", name, plugin)
on('loaded')
on('config_changed', config)
================================================
FILE: stenogotchi/plugins/cmd.py
================================================
# Handles the commandline stuff
import os
import logging
import glob
import re
import shutil
from fnmatch import fnmatch
from stenogotchi.plugins import default_path
SAVE_DIR = '/usr/local/share/stenogotchi/availaible-plugins/'
DEFAULT_INSTALL_PATH = '/usr/local/share/stenogotchi/installed-plugins/'
def add_parsers(parser):
"""
Adds the plugins subcommand to a given argparse.ArgumentParser
"""
subparsers = parser.add_subparsers()
## pwnagotchi plugins
parser_plugins = subparsers.add_parser('plugins')
plugin_subparsers = parser_plugins.add_subparsers(dest='plugincmd')
## pwnagotchi plugins search
parser_plugins_search = plugin_subparsers.add_parser('search', help='Search for stenogotchi plugins')
parser_plugins_search.add_argument('pattern', type=str, help="Search expression (wildcards allowed)")
## pwnagotchi plugins list
parser_plugins_list = plugin_subparsers.add_parser('list', help='List available stenogotchi plugins')
parser_plugins_list.add_argument('-i', '--installed', action='store_true', required=False, help='List also installed plugins')
## pwnagotchi plugins update
parser_plugins_update = plugin_subparsers.add_parser('update', help='Updates the database')
## pwnagotchi plugins upgrade
parser_plugins_upgrade = plugin_subparsers.add_parser('upgrade', help='Upgrades plugins')
parser_plugins_upgrade.add_argument('pattern', type=str, nargs='?', default='*', help="Filter expression (wildcards allowed)")
## pwnagotchi plugins enable
parser_plugins_enable = plugin_subparsers.add_parser('enable', help='Enables a plugin')
parser_plugins_enable.add_argument('name', type=str, help='Name of the plugin')
## pwnagotchi plugins disable
parser_plugins_disable = plugin_subparsers.add_parser('disable', help='Disables a plugin')
parser_plugins_disable.add_argument('name', type=str, help='Name of the plugin')
## pwnagotchi plugins install
parser_plugins_install = plugin_subparsers.add_parser('install', help='Installs a plugin')
parser_plugins_install.add_argument('name', type=str, help='Name of the plugin')
## pwnagotchi plugins uninstall
parser_plugins_uninstall = plugin_subparsers.add_parser('uninstall', help='Uninstalls a plugin')
parser_plugins_uninstall.add_argument('name', type=str, help='Name of the plugin')
## pwnagotchi plugins edit
parser_plugins_edit = plugin_subparsers.add_parser('edit', help='Edit the options')
parser_plugins_edit.add_argument('name', type=str, help='Name of the plugin')
return parser
def used_plugin_cmd(args):
"""
Checks if the plugins subcommand was used
"""
return hasattr(args, 'plugincmd')
def handle_cmd(args, config):
"""
Parses the arguments and does the thing the user wants
"""
if args.plugincmd == 'update':
return update(config)
elif args.plugincmd == 'search':
args.installed = True # also search in installed plugins
return list_plugins(args, config, args.pattern)
elif args.plugincmd == 'install':
return install(args, config)
elif args.plugincmd == 'uninstall':
return uninstall(args, config)
elif args.plugincmd == 'list':
return list_plugins(args, config)
elif args.plugincmd == 'enable':
return enable(args, config)
elif args.plugincmd == 'disable':
return disable(args, config)
elif args.plugincmd == 'upgrade':
return upgrade(args, config, args.pattern)
elif args.plugincmd == 'edit':
return edit(args, config)
raise NotImplementedError()
def edit(args, config):
pass
def enable(args, config):
pass
def disable(args, config):
pass
def upgrade(args, config, pattern='*'):
pass
def list_plugins(args, config, pattern='*'):
"""
Lists the available and installed plugins
"""
installed = _get_installed(config)
print(installed)
return 0
def _extract_version(filename):
"""
Extracts the version from a python file
"""
return None
def _get_available():
"""
Get all availaible plugins
"""
return None
def _get_installed(config):
"""
Get all installed plugins
"""
installed = dict()
search_dirs = [ default_path, config['main']['custom_plugins'] ]
for search_dir in search_dirs:
if search_dir:
for filename in glob.glob(os.path.join(search_dir, "*.py")):
plugin_name = os.path.basename(filename.replace(".py", ""))
installed[plugin_name] = filename
return installed
def uninstall(args, config):
"""
Uninstalls a plugin
"""
return 0
def install(args, config):
"""
Installs the given plugin
"""
return 0
def _analyse_dir(path):
return None
def update(config):
"""
Updates the database
"""
return 0
================================================
FILE: stenogotchi/plugins/default/buttonshim.py
================================================
# ###############################################################
# Updated 20-03-2021 by Anodynous
# - Moved all functionality into class to remove dependency on global variables and improve integration with other plugins and core Stenogotchi functionality.
#
# Updated 13-01-2021 by Anodynous
# - Added support for hold action in addition to press.
# Based on: https://github.com/evilsocket/pwnagotchi-plugins-contrib/blob/master/buttonshim.py
# which in turn is based on https://github.com/pimoroni/button-shim/commit/143f35b4b56626bd7062bdff3245658af19822b4
#
################################################################
import logging
import RPi.GPIO as GPIO
import subprocess
import signal
import smbus
import time
from threading import Thread
import atexit
from colorsys import hsv_to_rgb
import stenogotchi
import stenogotchi.plugins as plugins
try:
import queue
except ImportError:
import Queue as queue
ADDR = 0x3f
LED_DATA = 7
LED_CLOCK = 6
REG_INPUT = 0x00
REG_OUTPUT = 0x01
REG_POLARITY = 0x02
REG_CONFIG = 0x03
NUM_BUTTONS = 5
BUTTON_A = 0
"""Button A"""
BUTTON_B = 1
"""Button B"""
BUTTON_C = 2
"""Button C"""
BUTTON_D = 3
"""Button D"""
BUTTON_E = 4
"""Button E"""
NAMES = ['A', 'B', 'C', 'D', 'E']
"""Sometimes you want to print the plain text name of the button that's triggered.
You can use::
buttonshim.NAMES[button_index]
To accomplish this.
"""
ERROR_LIMIT = 10
FPS = 60
LED_GAMMA = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2,
2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5,
6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11, 11,
11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18,
19, 19, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 27, 28,
29, 29, 30, 31, 31, 32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40,
40, 41, 42, 43, 44, 45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
71, 72, 73, 74, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 88, 89,
90, 91, 93, 94, 95, 96, 98, 99, 100, 102, 103, 104, 106, 107, 109, 110,
111, 113, 114, 116, 117, 119, 120, 121, 123, 124, 126, 128, 129, 131, 132, 134,
135, 137, 138, 140, 142, 143, 145, 146, 148, 150, 151, 153, 155, 157, 158, 160,
162, 163, 165, 167, 169, 170, 172, 174, 176, 178, 179, 181, 183, 185, 187, 189,
191, 193, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220,
222, 224, 227, 229, 231, 233, 235, 237, 239, 241, 244, 246, 248, 250, 252, 255]
# The LED is an APA102 driven via the i2c IO expander.
# We must set and clear the Clock and Data pins
# Each byte in self._reg_queue represents a snapshot of the pin state
class Handler():
plugin = None
def __init__(self, plugin):
self.press = None
self.release = None
self.hold = None
self.hold_time = 0
self.repeat = False
self.repeat_time = 0
self.t_pressed = 0
self.t_repeat = 0
self.hold_fired = False
self.plugin = plugin
class Buttonshim(plugins.Plugin):
__author__ = 'gon@o2online.de, Anodynous'
__version__ = '0.0.2'
__license__ = 'GPL3'
__description__ = 'Pimoroni Button Shim GPIO Button and RGB LED support plugin based on the pimoroni-buttonshim-lib and the pwnagotchi-gpio-buttons-plugin'
def __init__(self):
self._agent = None
self.running = False
self.options = dict()
self._running = False
self._plover_wpm_meters_enabled = False
self._states = None
self._bus = None
self._reg_queue = []
self._update_queue = []
self._brightness = 0.5
self._led_queue = queue.Queue()
self._t_poll = None
self._running = False
self._states = 0b00011111
self._handlers = [None,None,None,None,None]
self._button_was_held = False
def on_loaded(self):
logging.info("[buttonshim] GPIO Button plugin loaded.")
self.running = True
self._handlers = [Handler(self) for x in range(NUM_BUTTONS)]
self.on_press([BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D, BUTTON_E], self.press_handler)
self.on_hold([BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D, BUTTON_E], self.hold_handler)
self.on_release([BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D, BUTTON_E], self.release_handler)
def on_config_changed(self, config):
self.config = config
def on_ready(self, agent):
self._agent = agent
def set_ui_update(self, key, value):
self._agent.view().set(key, value)
def trigger_ui_update(self):
self._agent.view().update()
def _run(self):
self._running = True
_last_states = 0b00011111
_errors = 0
while self._running:
led_data = None
try:
led_data = self._led_queue.get(False)
self._led_queue.task_done()
except queue.Empty:
pass
try:
if led_data:
for chunk in self._chunk(led_data, 32):
self._bus.write_i2c_block_data(ADDR, REG_OUTPUT, chunk)
self._states = self._bus.read_byte_data(ADDR, REG_INPUT)
except IOError:
_errors += 1
if _errors > ERROR_LIMIT:
self._running = False
raise IOError("More than {} IO errors have occurred!".format(ERROR_LIMIT))
for x in range(NUM_BUTTONS):
last = (_last_states >> x) & 1
curr = (self._states >> x) & 1
handler = self._handlers[x]
# If last > curr then it's a transition from 1 to 0
# since the buttons are active low, that's a press event
if last > curr:
handler.t_pressed = time.time()
handler.hold_fired = False
if callable(handler.press):
handler.t_repeat = time.time()
Thread(target=handler.press, args=(x, True, handler.plugin)).start()
continue
if last < curr and callable(handler.release):
Thread(target=handler.release, args=(x, False, handler.plugin)).start()
continue
if curr == 0:
if callable(handler.hold) and not handler.hold_fired and (time.time() - handler.t_pressed) > handler.hold_time:
Thread(target=handler.hold, args=(x,)).start()
handler.hold_fired = True
if handler.repeat and callable(handler.press) and (time.time() - handler.t_repeat) > handler.repeat_time:
self._handlers[x].t_repeat = time.time()
Thread(target=self._handlers[x].press, args=(x, True, handler.plugin)).start()
_last_states = self._states
time.sleep(1.0 / FPS)
def _quit(self):
if self._running:
self._led_queue.join()
self.set_pixel(0, 0, 0)
self._led_queue.join()
self._running = False
self._t_poll.join()
def setup(self):
if self._bus is not None:
return
try:
self._bus = smbus.SMBus(1)
self._bus.write_byte_data(ADDR, REG_CONFIG, 0b00011111)
self._bus.write_byte_data(ADDR, REG_POLARITY, 0b00000000)
self._bus.write_byte_data(ADDR, REG_OUTPUT, 0b00000000)
self._t_poll = Thread(target=self._run)
self._t_poll.daemon = True
self._t_poll.start()
self.set_pixel(0, 0, 0)
atexit.register(self._quit)
except OSError as ex:
logging.error(f"[buttonshim] Ignore if no ButtonSHIM hw module present and web UI enabled: OSError encountered during setup: {ex}")
def _set_bit(self, pin, value):
if value:
self._reg_queue[-1] |= (1 << pin)
else:
self._reg_queue[-1] &= ~(1 << pin)
def _next(self):
if len(self._reg_queue) == 0:
self._reg_queue = [0b00000000]
else:
self._reg_queue.append(self._reg_queue[-1])
def _enqueue(self):
self._led_queue.put(self._reg_queue)
self._reg_queue = []
def _chunk(self, l, n):
for i in range(0, len(l)+1, n):
yield l[i:i + n]
def _write_byte(self, byte):
for i in range(8):
self._next()
self._set_bit(LED_CLOCK, 0)
self._set_bit(LED_DATA, byte & 0b10000000)
self._next()
self._set_bit(LED_CLOCK, 1)
byte <<= 1
def on_hold(self, buttons, handler=None, hold_time=1):
"""Attach a hold handler to one or more buttons.
This handler is fired when you hold a button for hold_time seconds.
When fired it will run in its own Thread.
It will be passed one argument, the button index::
@buttonshim.on_hold(buttonshim.BUTTON_A)
def handler(button):
# Your code here
:param buttons: A single button, or a list of buttons
:param handler: Optional: a function to bind as the handler
:param hold_time: Optional: the hold time in seconds (default 2)
"""
self.setup()
if buttons is None:
buttons = [BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D, BUTTON_E]
if isinstance(buttons, int):
buttons = [buttons]
def attach_handler(handler):
for button in buttons:
self._handlers[button].hold = handler
self._handlers[button].hold_time = hold_time
if handler is not None:
attach_handler(handler)
else:
return attach_handler
def on_press(self, buttons, handler=None, repeat=False, repeat_time=0.5):
"""Attach a press handler to one or more buttons.
This handler is fired when you press a button.
When fired it will be run in its own Thread.
It will be passed two arguments, the button index and a
boolean indicating whether the button has been pressed/released::
@buttonshim.on_press(buttonshim.BUTTON_A)
def handler(button, pressed):
# Your code here
:param buttons: A single button, or a list of buttons
:param handler: Optional: a function to bind as the handler
:param repeat: Optional: Repeat the handler if the button is held
:param repeat_time: Optional: Time, in seconds, after which to repeat
"""
self.setup()
if buttons is None:
buttons = [BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D, BUTTON_E]
if isinstance(buttons, int):
buttons = [buttons]
def attach_handler(handler):
for button in buttons:
self._handlers[button].press = handler
self._handlers[button].repeat = repeat
self._handlers[button].repeat_time = repeat_time
if handler is not None:
attach_handler(handler)
else:
return attach_handler
def on_release(self, buttons=None, handler=None):
"""Attach a release handler to one or more buttons.
This handler is fired when you let go of a button.
When fired it will be run in its own Thread.
It will be passed two arguments, the button index and a
boolean indicating whether the button has been pressed/released::
@buttonshim.on_release(buttonshim.BUTTON_A)
def handler(button, pressed):
# Your code here
:param buttons: A single button, or a list of buttons
:param handler: Optional: a function to bind as the handler
"""
self.setup()
if buttons is None:
buttons = [BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D, BUTTON_E]
if isinstance(buttons, int):
buttons = [buttons]
def attach_handler(handler):
for button in buttons:
self._handlers[button].release = handler
if handler is not None:
attach_handler(handler)
else:
return attach_handler
def set_brightness(self, brightness):
self.setup()
if not isinstance(brightness, int) and not isinstance(brightness, float):
raise ValueError("Brightness should be an int or float")
if brightness < 0.0 or brightness > 1.0:
raise ValueError("Brightness should be between 0.0 and 1.0")
self._brightness = brightness
def set_pixel(self, r, g, b):
"""Set the Button SHIM RGB pixel
Display an RGB colour on the Button SHIM pixel.
:param r: Amount of red, from 0 to 255
:param g: Amount of green, from 0 to 255
:param b: Amount of blue, from 0 to 255
You can use HTML colours directly with hexadecimal notation in Python. EG::
buttonshim.self.set_pixel(0xFF, 0x00, 0xFF)
"""
self.setup()
if not isinstance(r, int) or r < 0 or r > 255:
raise ValueError("Argument r should be an int from 0 to 255")
if not isinstance(g, int) or g < 0 or g > 255:
raise ValueError("Argument g should be an int from 0 to 255")
if not isinstance(b, int) or b < 0 or b > 255:
raise ValueError("Argument b should be an int from 0 to 255")
r, g, b = [int(x * self._brightness) for x in (r, g, b)]
self._write_byte(0)
self._write_byte(0)
self._write_byte(0b11101111)
self._write_byte(LED_GAMMA[b & 0xff])
self._write_byte(LED_GAMMA[g & 0xff])
self._write_byte(LED_GAMMA[r & 0xff])
self._write_byte(0)
self._write_byte(0)
self._enqueue()
def blink(self, r, g, b, ontime, offtime, blinktimes):
logging.debug("[buttonshim] Blink")
for i in range(0, blinktimes):
self.set_pixel(r, g, b)
time.sleep(ontime)
self.set_pixel(0, 0, 0)
time.sleep(offtime)
def press_handler(self, button, pressed, plugin):
""" On press reset button held status """
self._button_was_held = False
def toggle_qwerty_steno(self):
try:
cap_state = plugins.loaded['evdevkb'].get_capture_state()
if not cap_state:
plugins.loaded['evdevkb'].start_capture()
logging.info(f"[buttonshim] Switched to QWERTY mode")
else:
plugins.loaded['evdevkb'].stop_capture()
logging.info(f"[buttonshim] Switched to STENO mode")
except Exception as ex:
logging.exception(f"[buttonshim] Check if evdevkb is loaded, exception: {str(ex)}")
def toggle_wpm_meters(self):
command = {}
try:
wpm_method = plugins.loaded['plover_link'].options['wpm_method']
wpm_timeout = plugins.loaded['plover_link'].options['wpm_timeout']
except Exception as ex:
logging.exception(f"[buttonshim] Check that wpm_method and wpm_timeout is configured. Falling back to defaults. Exception: {str(ex)}")
wpm_method = 'ncra'
wpm_timeout = '60'
if self._plover_wpm_meters_enabled:
command = {'stop_wpm_meter': 'wpm and strokes'}
self.set_ui_update('wpm', '')
self.set_ui_update('strokes', '')
self.trigger_ui_update()
logging.info(f"[buttonshim] Disabled WPM readings")
elif not self._plover_wpm_meters_enabled:
command = {'start_wpm_meter': 'wpm and strokes',
'wpm_method' : wpm_method,
'wpm_timeout' : wpm_timeout}
self.set_ui_update('wpm', wpm_method)
self.set_ui_update('strokes', f"{wpm_timeout}s")
self.trigger_ui_update()
logging.info(f"[buttonshim] Enabled WPM readings using method {wpm_method} and timeout {wpm_timeout}")
self._plover_wpm_meters_enabled = not self._plover_wpm_meters_enabled
plugins.loaded['plover_link'].send_signal_to_plover(command)
def toggle_dictionary_lookup(self):
if plugins.loaded['dict_lookup'].get_running():
if not plugins.loaded['dict_lookup'].get_input_mode(): # If not currently enabled, enable input mode
plugins.loaded['dict_lookup'].enable_input_mode()
else: # If currently enabled, revert to normal view
plugins.loaded['dict_lookup'].disable_input_mode()
else:
logging.debug(f"[buttonshim] dict_lookup is not ready yet. Check that Plover is running.")
def hold_handler(self, button):
""" On long press run built in internal Stenogotchi commands """
# Set button held status to prevent release_handler from triggering on release
self._button_was_held = True
# Blink in response to long hold event
red = 0
green = 70
blue = 70
on_time = 1
off_time = 0
blink_times = 1
thread = Thread(target=self.blink, args=(red, green, blue, on_time, off_time, blink_times))
thread.start()
if NAMES[button] == 'A':
# Toggle QWERTY/STENO mode
self.toggle_qwerty_steno()
elif NAMES[button] == 'B':
# Toggle WPM & strokes meters for Plover
self.toggle_wpm_meters()
elif NAMES[button] == 'C':
# Toggle dictionary lookup mode
self.toggle_dictionary_lookup()
elif NAMES[button] == 'D':
# Toggle wifi on/off
stenogotchi.set_wifi_onoff()
# Check for changes in wifi status over a short while
for i in range(5):
self._agent._update_wifi()
time.sleep(2)
logging.info(f"[buttonshim] Toggled wifi state")
elif NAMES[button] == 'E':
# Initiate clean shutdown process
logging.info(f"[buttonshim] Initiated clean shutdown")
stenogotchi.shutdown()
def release_handler(self, button, pressed, plugin):
""" On short press run command from config """
if not self._button_was_held:
logging.info(f"[buttonshim] Button Pressed! Loading command from slot '{button}' for button '{NAMES[button]}'")
bCfg = plugin.options['buttons'][NAMES[button]]
blinkCfg = bCfg['blink']
logging.debug(f'[buttonshim] {self.blink}')
if blinkCfg['enabled'] == True:
logging.debug(f"[buttonshim] Blinking led")
red = int(blinkCfg['red'])
green = int(blinkCfg['green'])
blue = int(blinkCfg['blue'])
on_time = float(blinkCfg['on_time'])
off_time = float(blinkCfg['off_time'])
blink_times = int(blinkCfg['blink_times'])
logging.debug(f"[buttonshim] red {red} green {green} blue {blue} on_time {on_time} off_time {off_time} blink_times {blink_times}")
thread = Thread(target=self.blink, args=(red, green, blue, on_time, off_time, blink_times))
thread.start()
logging.debug(f"[buttonshim] Blink thread started")
command = bCfg['command']
if command == '':
logging.debug(f"[buttonshim] Command empty")
else:
logging.debug(f"[buttonshim] Process create: {command}")
process = subprocess.Popen(command, shell=True, stdin=None, stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
process.wait()
process = None
logging.debug(f"[buttonshim] Process end")
================================================
FILE: stenogotchi/plugins/default/dict_lookup.py
================================================
#!/usr/bin/env python3
import logging
import time
import random
import stenogotchi.plugins as plugins
from stenogotchi.ui.components import LabeledValue, Text, Line
from stenogotchi.ui.view import BLACK
from stenogotchi.ui import view, state, faces
import stenogotchi.ui.fonts as fonts
class UiHandler():
def __init__(self, agent):
self.clear_elements = ('name', 'ups', 'wpm', 'status', 'strokes', 'uptime', 'line1')
self.input_mode = False
self._agent = agent
self._view = view.ROOT
self._stored_state = state.State()
self.minion_font = fonts.ImageFont.truetype("%s-Bold" % fonts.FONT_NAME, 18)
self.minion_offset = 65
def store_state(self):
for key, element in self._view._state.items():
if key in self.clear_elements or key == 'face':
self._stored_state.add_element(key, element)
def restore_state(self):
# Add all removed items back
for key, element in self._stored_state.items():
if key in self.clear_elements:
self.add_element(key, element)
def surprise_exit(self):
if not self._view.get('face') == faces.LOOK_L:
self._view.set('face', faces.LOOK_L)
self.update_view()
time.sleep(1)
self._view.set('face', faces.LOOK_R)
self.update_view()
time.sleep(1)
def relocate_face(self, minion):
self.remove_element('face')
if minion:
# Shrink and relocate
face = random.choices((faces.LOOK_L_HAPPY, faces.LOOK_L), weights=(0.9, 0.10), k=1)
self.add_element('face', Text(value=face[0], position=((self._view._width - self.minion_offset + 2), 0), color=BLACK, font=self.minion_font))
else:
# Grow and relocate, state doesn't matter as we will update it before view refresh
self.add_element('face', self._stored_state._state['face'])
def check_element(self, key):
return self._view.has_element(key)
def remove_element(self, key):
try:
self._view.remove_element(key)
except KeyError:
# TODO: fix self._view.has_element to replace this. Always seems to return None right now.
logging.error(f"[dict_lookup] No element '{key}' available for removal")
def add_element(self, key, element):
self._view.add_element(key, element)
def update_view(self):
self._view.update()
def enable_input_mode(self):
# Storing current state
self.store_state()
# Removing elements we will be overlapping and block them from triggering ui updates
for element in self.clear_elements:
self.remove_element(element)
# Block update of ui elements we removed
self._view._ignore_changes = self.clear_elements
# Shrink stenogotchi face
self.relocate_face(minion=True)
# Add new ui elements for input and output
line1_offset = self._view._layout['line1'].copy()
line1_offset[2] = self._view._width - self.minion_offset
self.add_element('input', LabeledValue(color=BLACK, label='', value='', position=(0, 0) , label_font=fonts.Bold, text_font=fonts.Medium, max_length=29))
self.add_element('line1_offset', Line(line1_offset, color=BLACK))
self.add_element('out1', Text(value='', position=self._agent._view._layout['name'], color=BLACK, font=fonts.Bold, wrap=True, max_length=self._agent._view._layout['status']['max']-1))
self.add_element('out2', Text(value='', position=self._agent._view._layout['status']['pos'], color=BLACK, font=self._agent._view._layout['status']['font'], wrap=True, max_length=self._agent._view._layout['status']['max']))
self.input_mode = True
# Used instead of self.update_view() to indicate input position from the start
self.display_input("")
logging.info("[dict_lookup] Enabled dictionary lookup mode")
def disable_input_mode(self):
# Add some variety to the exit
if random.randint(0, 9) > 7:
self.surprise_exit()
self._view.remove_element('line1_offset')
self._view.remove_element('input')
self._view.remove_element('out1')
self._view.remove_element('out2')
# Restore removed elements
self.restore_state()
# Grow stenogotchi face
self.relocate_face(minion=False)
# revert default update-ignore state
if self._view._config['ui']['fps'] > 0.0:
self._view._ignore_changes = ()
else:
self._view._ignore_changes = ('uptime', 'name')
# Prepare new empty state object
self._stored_state = state.State()
# Refresh uptime, set return message which will trigger ui update
self._agent._update_uptime()
self._agent.set_on_dict_lookup_done()
self.input_mode = False
logging.info("[dict_lookup] Disabled dictionary lookup mode")
def get_input_mode(self):
return self.input_mode
def display_output(self, list):
if self.get_input_mode():
out1_str = ""
out2_str = ""
#TODO: handle list items longer than max supported length for column
cnt = 0
for item in list:
if cnt < 6: # lines 0-5 fit in first column
out1_str += f"{item}\n"
elif cnt < 13: # lines 6-11 fit in second column
out2_str += f"{item}\n"
else:
break # no more room
cnt += 1
self._view.set('out1', out1_str)
self._view.set('out2', out2_str)
self.update_view()
def display_input(self, string, position_indicator="_"):
if self.get_input_mode():
string += position_indicator # Character added to highlight input position
self._agent._view.set('input', string)
self.update_view()
class InputHandler():
# TODO: Add support for evdevkb as input device
def __init__(self, agent):
self._agent = agent
self.input_mode = False
self._input = ""
def _on_send_string(self, text: str):
# If enter key received
if text in ('\n', '\r', '\r\n'):
if plugins.loaded['dict_lookup'].get_input_mode():
self._input += " "
self.push_input(position_indicator="")
plugins.loaded['dict_lookup'].lookup_word(self._input)
else:
self._input += text
self.push_input()
def _on_send_backspaces(self, count: int):
if count >= len(self._input):
self.clear_input()
else:
self._input = self._input[:-count]
self.push_input()
def _on_send_key_combination(self, combination: str):
if combination == 'Control_L(BackSpace)':
self.clear_input()
else:
logging.warning(f"[dict_lookup] Key-combinations not supported. Input '{combination}' ignored")
def _on_lookup_results(self, results_list):
# logging.debug(f"[dict_lookup] Lookup result from Plover '{results_list}'")
plugins.loaded['dict_lookup'].display_lookup_result(results_list)
def enable_input_mode(self):
# TODO: trigger routing from evdevkb to here
command = {'output_to_stenogotchi': True}
plugins.loaded['plover_link'].send_signal_to_plover(command)
self.input_mode = True
def disable_input_mode(self):
# TODO: revert routing from evdevkb to btclient
command = {'output_to_stenogotchi': False}
plugins.loaded['plover_link'].send_signal_to_plover(command)
self.input_mode = False
def push_input(self, position_indicator="_"):
plugins.loaded['dict_lookup'].push_input(self._input, position_indicator)
def clear_input(self):
self._input = ""
self.push_input()
class DictLookup(plugins.Plugin):
__autohor__ = 'Anodynous'
__version__ = '0.3'
__license__ = 'GPL3'
__description__ = 'This plugin enables looking up words and strokes in enabled plover dictionaries'
def __init__(self):
self._agent = None
self.config = None
self.running = False
self.input_mode = False
self.input_handler = None
self.ui_handler = None
def on_plover_ready(self, agent):
self._agent = agent
self.ui_handler = UiHandler(agent)
self.input_handler = InputHandler(agent)
self.running = True
def on_config_changed(self, config):
self.config = config
def on_unload(self, ui):
self.lookup = None
self.ui_handler = None
def get_running(self):
return self.running
def get_input_mode(self):
return self.input_mode
def enable_input_mode(self):
self.ui_handler.enable_input_mode()
self.input_handler.enable_input_mode()
self.input_mode = True
def disable_input_mode(self):
self.ui_handler.disable_input_mode()
self.input_handler.disable_input_mode()
self.input_handler.clear_input()
self.input_mode = False
def lookup_word(self, word):
# Remove leading/trailing whitespaces and send word to plover for dictionary lookup
word = word.strip()
command = {'lookup_word': word}
plugins.loaded['plover_link'].send_signal_to_plover(command)
def lookup_stroke(self, stroke):
# Remove leading/trailing whitespaces and send stroke to plover for dictionary lookup
# TODO: implement functionality to use stroke-lookup
stroke = stroke.strip()
logging.debug(f"[dict_lookup] Looking up stroke '{stroke}'")
command = {'lookup_word': stroke}
plugins.loaded['plover_link'].send_signal_to_plover(command)
def display_lookup_result(self, results_list):
self.push_output(results_list)
def sort_list(self, rlist):
# Sorts the results in ascending order.
# Primary sorting key: number of chords
# Secondary sorting key: length of chord(combination)
sort_key = lambda k : (k.count('/'), len(k))
rlist_sorted = []
if len(rlist) < 2:
return rlist
else:
rlist_sorted = sorted(rlist, key=sort_key)
return rlist_sorted
def push_input(self, string="", position_indicator="_"):
if self.input_mode:
self.ui_handler.display_input(string, position_indicator)
def push_output(self, rlist):
if self.input_mode:
if rlist:
rlist_sorted = self.sort_list(rlist)
else:
rlist_sorted = []
self.ui_handler.display_output(rlist_sorted)
if __name__ == '__main__':
print("Please enable and run as Stenogotchi plugin")
================================================
FILE: stenogotchi/plugins/default/evdevkb.py
================================================
"""
Evdev based keyboard client for capturing input and relays the keypress to a Bluetooth HID keyboard emulator D-BUS Service
Based on: https://gist.github.com/ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0#file-readme-md
Which in turn takes the original idea from: http://yetanotherpointlesstechblog.blogspot.com/2016/04/emulating-bluetooth-keyboard-with.html
Tested on:
Python 3.7 (needs 3.4+)
Evdev 1.3.0
"""
import logging
import evdev
from select import select
from time import sleep
if not __name__ == '__main__':
import stenogotchi.plugins as plugins
ObjectClass = plugins.Plugin
else:
import dbus
ObjectClass = object
HID_DBUS = 'com.github.stenogotchi'
HID_SRVC = '/com/github/stenogotchi'
KEYTABLE = {
"KEY_RESERVED": 0,
"KEY_ESC": 41,
"KEY_1": 30,
"KEY_2": 31,
"KEY_3": 32,
"KEY_4": 33,
"KEY_5": 34,
"KEY_6": 35,
"KEY_7": 36,
"KEY_8": 37,
"KEY_9": 38,
"KEY_0": 39,
"KEY_MINUS": 45,
"KEY_EQUAL": 46,
"KEY_BACKSPACE": 42,
"KEY_TAB": 43,
"KEY_Q": 20,
"KEY_W": 26,
"KEY_E": 8,
"KEY_R": 21,
"KEY_T": 23,
"KEY_Y": 28,
"KEY_U": 24,
"KEY_I": 12,
"KEY_O": 18,
"KEY_P": 19,
"KEY_LEFTBRACE": 47,
"KEY_RIGHTBRACE": 48,
"KEY_ENTER": 40,
"KEY_LEFTCTRL": 224,
"KEY_A": 4,
"KEY_S": 22,
"KEY_D": 7,
"KEY_F": 9,
"KEY_G": 10,
"KEY_H": 11,
"KEY_J": 13,
"KEY_K": 14,
"KEY_L": 15,
"KEY_SEMICOLON": 51,
"KEY_APOSTROPHE": 52,
"KEY_GRAVE": 53,
"KEY_LEFTSHIFT": 225,
"KEY_BACKSLASH": 50,
"KEY_Z": 29,
"KEY_X": 27,
"KEY_C": 6,
"KEY_V": 25,
"KEY_B": 5,
"KEY_N": 17,
"KEY_M": 16,
"KEY_COMMA": 54,
"KEY_DOT": 55,
"KEY_SLASH": 56,
"KEY_RIGHTSHIFT": 229,
"KEY_KPASTERISK": 85,
"KEY_LEFTALT": 226,
"KEY_SPACE": 44,
"KEY_CAPSLOCK": 57,
"KEY_F1": 58,
"KEY_F2": 59,
"KEY_F3": 60,
"KEY_F4": 61,
"KEY_F5": 62,
"KEY_F6": 63,
"KEY_F7": 64,
"KEY_F8": 65,
"KEY_F9": 66,
"KEY_F10": 67,
"KEY_NUMLOCK": 83,
"KEY_SCROLLLOCK": 71,
"KEY_KP7": 95,
"KEY_KP8": 96,
"KEY_KP9": 97,
"KEY_KPMINUS": 86,
"KEY_KP4": 92,
"KEY_KP5": 93,
"KEY_KP6": 94,
"KEY_KPPLUS": 87,
"KEY_KP1": 89,
"KEY_KP2": 90,
"KEY_KP3": 91,
"KEY_KP0": 98,
"KEY_KPDOT": 99,
"KEY_ZENKAKUHANKAKU": 148,
"KEY_102ND": 100,
"KEY_F11": 68,
"KEY_F12": 69,
"KEY_RO": 135,
"KEY_KATAKANA": 146,
"KEY_HIRAGANA": 147,
"KEY_HENKAN": 138,
"KEY_KATAKANAHIRAGANA": 136,
"KEY_MUHENKAN": 139,
"KEY_KPJPCOMMA": 140,
"KEY_KPENTER": 88,
"KEY_RIGHTCTRL": 228,
"KEY_KPSLASH": 84,
"KEY_SYSRQ": 70,
"KEY_RIGHTALT": 230,
"KEY_HOME": 74,
"KEY_UP": 82,
"KEY_PAGEUP": 75,
"KEY_LEFT": 80,
"KEY_RIGHT": 79,
"KEY_END": 77,
"KEY_DOWN": 81,
"KEY_PAGEDOWN": 78,
"KEY_INSERT": 73,
"KEY_DELETE": 76,
"KEY_MUTE": 239,
"KEY_VOLUMEDOWN": 238,
"KEY_VOLUMEUP": 237,
"KEY_POWER": 102,
"KEY_KPEQUAL": 103,
"KEY_PAUSE": 72,
"KEY_KPCOMMA": 133,
"KEY_HANGEUL": 144,
"KEY_HANJA": 145,
"KEY_YEN": 137,
"KEY_LEFTMETA": 227,
"KEY_RIGHTMETA": 231,
"KEY_COMPOSE": 101,
"KEY_STOP": 243,
"KEY_AGAIN": 121,
"KEY_PROPS": 118,
"KEY_UNDO": 122,
"KEY_FRONT": 119,
"KEY_COPY": 124,
"KEY_OPEN": 116,
"KEY_PASTE": 125,
"KEY_FIND": 244,
"KEY_CUT": 123,
"KEY_HELP": 117,
"KEY_CALC": 251,
"KEY_SLEEP": 248,
"KEY_WWW": 240,
"KEY_COFFEE": 249,
"KEY_BACK": 241,
"KEY_FORWARD": 242,
"KEY_EJECTCD": 236,
"KEY_NEXTSONG": 235,
"KEY_PLAYPAUSE": 232,
"KEY_PREVIOUSSONG": 234,
"KEY_STOPCD": 233,
"KEY_REFRESH": 250,
"KEY_EDIT": 247,
"KEY_SCROLLUP": 245,
"KEY_SCROLLDOWN": 246,
"KEY_F13": 104,
"KEY_F14": 105,
"KEY_F15": 106,
"KEY_F16": 107,
"KEY_F17": 108,
"KEY_F18": 109,
"KEY_F19": 110,
"KEY_F20": 111,
"KEY_F21": 112,
"KEY_F22": 113,
"KEY_F23": 114,
"KEY_F24": 115
}
# Map modifier keys to array element in the bit array
MODKEYS = {
"KEY_RIGHTMETA": 0,
"KEY_RIGHTALT": 1,
"KEY_RIGHTSHIFT": 2,
"KEY_RIGHTCTRL": 3,
"KEY_LEFTMETA": 4,
"KEY_LEFTALT": 5,
"KEY_LEFTSHIFT": 6,
"KEY_LEFTCTRL": 7
}
class EvdevKbrd:
"""
Take the events from a physically attached keyboard and send the
HID messages to the keyboard D-Bus server.
"""
def __init__(self, skip_dbus = False):
self._skip_dbus = skip_dbus
self.do_capture = False
self.keytable = KEYTABLE
self.modkeys = MODKEYS
self.target_length = 6
self.mod_keys = 0b00000000
self.pressed_keys = []
self.have_kb = False
self.devs = None
if self._skip_dbus:
self.bus = None
self.btkobject = None
self.btk_service = None
else:
self.bus = dbus.SystemBus()
self.btkobject = self.bus.get_object(HID_DBUS, HID_SRVC)
self.btk_service = dbus.Interface(self.btkobject, HID_DBUS)
def convert(self, evdev_keycode):
return self.keytable[evdev_keycode]
def modkey(self, evdev_keycode):
if evdev_keycode in self.modkeys:
return self.modkeys[evdev_keycode]
else:
return -1 # Return an invalid array element
def set_do_capture(self, toggle):
self.do_capture = toggle
def grab(self):
# Make input device unavailable for other applications
for dev in self.devs:
dev.grab()
def ungrab(self):
# Release input device for other applications
for dev in self.devs:
dev.ungrab()
def get_input_devices(self):
# Returns all input devices connected to device
input_devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
return input_devices
def get_keyboards(self):
# Returns all input devices that look like keyboards that are connected to device
input_devices = self.get_input_devices()
keyboards = []
for device in input_devices:
# Check if the input device has a KEY_A
has_key_a = evdev.ecodes.KEY_A in device.capabilities().get(evdev.ecodes.EV_KEY, [])
if has_key_a:
keyboards.append(device)
logging.debug(f"[evdevkb] Found keyboard '{device.name}' at path '{device.path}'")
return keyboards
def set_keyboards(self):
# Sets all keyboards as device to listen for key-inputs from
while not self.have_kb:
if not self.do_capture:
break
keyboards = self.get_keyboards()
if keyboards:
self.devs = keyboards
self.have_kb = True
else:
logging.debug('[evdevkb] Keyboard not found, waiting 3 seconds and retrying')
sleep(3)
def update_mod_keys(self, mod_key, value):
"""
Which modifier keys are active is stored in an 8 bit number.
Each bit represents a different key. This method takes which bit
and its new value as input
:param mod_key: The value of the bit to be updated with new value
:param value: Binary 1 or 0 depending if pressed or released
"""
bit_mask = 1 << (7-mod_key)
if value: # set bit
self.mod_keys |= bit_mask
else: # clear bit
self.mod_keys &= ~bit_mask
def update_keys(self, norm_key, value):
if value < 1:
self.pressed_keys.remove(norm_key)
elif norm_key not in self.pressed_keys:
self.pressed_keys.insert(0, norm_key)
len_delta = self.target_length - len(self.pressed_keys)
if len_delta < 0:
self.pressed_keys = self.pressed_keys[:len_delta]
elif len_delta > 0:
self.pressed_keys.extend([0] * len_delta)
@property
def state(self):
"""
property with the HID message to send for the current keys pressed
on the keyboards
:return: bytes of HID message
"""
return [0xA1, 0x01, self.mod_keys, 0, *self.pressed_keys]
def send_keys(self):
# If ran as part of Stenogotchi, communicate directly with plugin
if self._skip_dbus:
plugins.loaded['plover_link']._stenogotchiservice.send_keys([self.state])
# If ran as stand-alone, assume dbus is needed to access send_keys() function
else:
self.btk_service.send_keys(self.state)
def event_loop(self):
"""
Reads keypresses from all identified keyboards and sends them to emulated
bluetooth HID as long as do_capture is True.
"""
self.do_capture = True
self.grab()
while self.do_capture:
r, w, x = select(self.devs, [], [], 0.01) # 0.1 is default
for fd in r:
for event in fd.read():
# We only want up/down key-events
if event.type == evdev.ecodes.EV_KEY and event.value < 2:
key_str = evdev.ecodes.KEY[event.code]
mod_key = self.modkey(key_str)
if mod_key > -1:
self.update_mod_keys(mod_key, event.value)
else:
self.update_keys(self.convert(key_str), event.value)
self.send_keys()
self.ungrab()
for dev in self.devs:
dev.close()
class EvdevKeyboard(ObjectClass):
__autohor__ = 'Anodynous'
__version__ = '0.2'
__license__ = 'MIT'
__description__ = 'This plugin captures and blocks keypress events using evdev and sends to module emulating bluetooth HID device.'
def __init__(self):
self._agent = None
self.evdevkb = None
self.do_capture = False
def on_ready(self, agent):
self._agent = agent
def on_config_changed(self, config):
self.config = config
def trigger_ui_update(self, input_mode):
self._agent.view().set('mode', input_mode)
self._agent.view().update()
def start_capture(self):
logging.info('[evdevkb] Capturing evdev keypress events...')
self.trigger_ui_update('QWERTY')
self.evdevkb = EvdevKbrd(skip_dbus=True)
self.evdevkb.set_do_capture(True)
self.do_capture = True
self.evdevkb.set_keyboards()
self.evdevkb.event_loop()
def stop_capture(self):
logging.info('[evdevkb] Ignoring evdev keypress events...')
self.evdevkb.set_do_capture(False)
self.do_capture = False
self.evdevkb = None
self.trigger_ui_update('STENO')
def get_capture_state(self):
return self.do_capture
if __name__ == '__main__':
try:
print('Setting up keyboard')
kb = EvdevKbrd()
print('starting event loop')
kb.set_keyboards()
kb.event_loop()
except RuntimeError: pass # Handling for bug in evdev 1.3.0, see https://github.com/gvalkov/python-evdev/issues/120
================================================
FILE: stenogotchi/plugins/default/example.py
================================================
# ###############################################################
# Based on: https://github.com/evilsocket/pwnagotchi/blob/master/pwnagotchi/plugins/default/example.py
#
# Changed 22-03-2021 by Anodynous
# - Changed to fit Stenogotchi events
#
################################################################
import logging
import stenogotchi.plugins as plugins
from stenogotchi.ui.components import LabeledValue
from stenogotchi.ui.view import BLACK
import stenogotchi.ui.fonts as fonts
class Example(plugins.Plugin):
__author__ = 'evilsocket@gmail.com, Anodynous'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'An example plugin for pwnagotchi that implements all the available callbacks.'
def __init__(self):
logging.debug("[example] example plugin created")
# called when http://<host>:<port>/plugins/<plugin>/ is called
# must return a html page
# IMPORTANT: If you use "POST"s, add a csrf-token (via csrf_token() and render_template_string)
def on_webhook(self, path, request):
logging.info("[example] webhook established")
# called when the plugin is loaded
def on_loaded(self):
logging.warning("[example] WARNING: this plugin should be disabled! options = " % self.options)
# called before the plugin is unloaded
def on_unload(self, ui):
logging.info("[example] is unloaded")
# called hen there's internet connectivity
def on_internet_available(self, agent):
logging.info("[example] unit has internet connection")
# called to setup the ui elements
def on_ui_setup(self, ui):
# add custom UI elements
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 - 25, 0),
label_font=fonts.Bold, text_font=fonts.Medium))
# called when the ui is updated
def on_ui_update(self, ui):
# update those elements
some_voltage = 0.1
some_capacity = 100.0
ui.set('ups', "%4.2fV/%2i%%" % (some_voltage, some_capacity))
# called when the hardware display setup is done, display is an hardware specific object
def on_display_setup(self, display):
logging.info("[example] unit has set up display")
# called when everything is ready and the main loop is about to start
def on_ready(self, agent):
logging.info("[example] unit is ready")
# you can run custom bettercap commands if you want
# agent.run('ble.recon on')
# or set a custom state
# agent.set_bored()
# called when the status is set to grateful
def on_grateful(self, agent):
logging.info("[example] unit is grateful")
# called when the status is set to lonely
def on_lonely(self, agent):
logging.info("[example] unit is lonely")
# called when the status is set to bored
def on_bored(self, agent):
logging.info("[example] unit is bored")
# called when the status is set to sad
def on_sad(self, agent):
logging.info("[example] unit is sad")
# called when the status is set to angry
def on_angry(self, agent):
logging.info("[example] unit is angry")
# called when the status is set to excited
def on_excited(self, agent):
logging.info("[example] unit is excited")
# called when the agent is rebooting the board
def on_rebooting(self, agent):
logging.info("[example] unit is rebooting")
# called when the agent is waiting for t seconds
def on_wait(self, agent, t):
logging.info(f"[example] unit is waiting {t} seconds")
# called when the agent is sleeping for t seconds
def on_sleep(self, agent, t):
logging.info(f"[example] unit is sleeping {t} seconds")
# called when an epoch is over (where an epoch is a single loop of the main algorithm)
def on_epoch(self, agent, epoch, epoch_data):
logging.info(f"[example] epoch is over")
# called when successfully connected to a bluetooth host
def on_bt_connected(self, agent, bthost_name):
logging.info(f"[example] unit connected to bluetooth host '{bthost_name}'")
# called when disconnected from bluetooth host
def on_bt_disconnected(self, agent):
logging.info("[example] unit disconnected from bluetooth host")
# called when plover boots
def on_plover_boot(self, agent):
logging.info("[example] Plover is starting up")
# called when plover is ready
def on_plover_ready(self, agent):
logging.info("[example] Plover is ready")
# called when plover quits
def on_plover_quit(self, agent):
gitextract_1w8uxzjn/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── BUILDNOTES.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── initial_setup.sh ├── plover_plugin/ │ ├── setup.cfg │ ├── setup.py │ └── stenogotchi_link/ │ ├── __init__.py │ ├── clients.py │ ├── com.github.stenogotchi.conf │ ├── keymap.py │ ├── stenogotchi_link.py │ └── wpm.py ├── requirements.txt ├── stenogotchi/ │ ├── __init__.py │ ├── _version.py │ ├── agent.py │ ├── automata.py │ ├── defaults.toml │ ├── fs/ │ │ └── __init__.py │ ├── log.py │ ├── plugins/ │ │ ├── __init__.py │ │ ├── cmd.py │ │ └── default/ │ │ ├── buttonshim.py │ │ ├── dict_lookup.py │ │ ├── evdevkb.py │ │ ├── example.py │ │ ├── led.py │ │ ├── logtail.py │ │ ├── memtemp.py │ │ ├── plover_link.py │ │ ├── plover_link_btserver_sdp_record.xml │ │ └── upslite.py │ ├── ui/ │ │ ├── __init__.py │ │ ├── components.py │ │ ├── display.py │ │ ├── faces.py │ │ ├── fonts.py │ │ ├── hw/ │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── libs/ │ │ │ │ ├── __init__.py │ │ │ │ └── waveshare/ │ │ │ │ ├── __init__.py │ │ │ │ └── v2/ │ │ │ │ ├── __init__.py │ │ │ │ └── epd2in13_V2.py │ │ │ └── waveshare2.py │ │ ├── state.py │ │ ├── view.py │ │ └── web/ │ │ ├── __init__.py │ │ ├── handler.py │ │ ├── server.py │ │ ├── static/ │ │ │ ├── css/ │ │ │ │ ├── jquery.jqplot.css │ │ │ │ └── style.css │ │ │ └── js/ │ │ │ ├── jquery.jqplot.js │ │ │ ├── jquery.mobile/ │ │ │ │ ├── jquery.mobile-1.4.5.css │ │ │ │ ├── jquery.mobile-1.4.5.js │ │ │ │ ├── jquery.mobile.external-png-1.4.5.css │ │ │ │ ├── jquery.mobile.icons-1.4.5.css │ │ │ │ ├── jquery.mobile.inline-png-1.4.5.css │ │ │ │ ├── jquery.mobile.inline-svg-1.4.5.css │ │ │ │ ├── jquery.mobile.structure-1.4.5.css │ │ │ │ └── jquery.mobile.theme-1.4.5.css │ │ │ ├── jquery.timeago.js │ │ │ ├── plugins/ │ │ │ │ ├── jqplot.BezierCurveRenderer.js │ │ │ │ ├── jqplot.barRenderer.js │ │ │ │ ├── jqplot.blockRenderer.js │ │ │ │ ├── jqplot.bubbleRenderer.js │ │ │ │ ├── jqplot.canvasAxisLabelRenderer.js │ │ │ │ ├── jqplot.canvasAxisTickRenderer.js │ │ │ │ ├── jqplot.canvasOverlay.js │ │ │ │ ├── jqplot.canvasTextRenderer.js │ │ │ │ ├── jqplot.categoryAxisRenderer.js │ │ │ │ ├── jqplot.ciParser.js │ │ │ │ ├── jqplot.cursor.js │ │ │ │ ├── jqplot.dateAxisRenderer.js │ │ │ │ ├── jqplot.donutRenderer.js │ │ │ │ ├── jqplot.dragable.js │ │ │ │ ├── jqplot.enhancedLegendRenderer.js │ │ │ │ ├── jqplot.enhancedPieLegendRenderer.js │ │ │ │ ├── jqplot.funnelRenderer.js │ │ │ │ ├── jqplot.highlighter.js │ │ │ │ ├── jqplot.json2.js │ │ │ │ ├── jqplot.logAxisRenderer.js │ │ │ │ ├── jqplot.mekkoAxisRenderer.js │ │ │ │ ├── jqplot.mekkoRenderer.js │ │ │ │ ├── jqplot.meterGaugeRenderer.js │ │ │ │ ├── jqplot.mobile.js │ │ │ │ ├── jqplot.ohlcRenderer.js │ │ │ │ ├── jqplot.pieRenderer.js │ │ │ │ ├── jqplot.pointLabels.js │ │ │ │ ├── jqplot.pyramidAxisRenderer.js │ │ │ │ ├── jqplot.pyramidGridRenderer.js │ │ │ │ ├── jqplot.pyramidRenderer.js │ │ │ │ └── jqplot.trendline.js │ │ │ └── viewportHeight.js │ │ └── templates/ │ │ ├── base.html │ │ ├── index.html │ │ ├── plugins.html │ │ └── status.html │ ├── utils.py │ └── voice.py └── stenogotchi.py
SYMBOL INDEX (862 symbols across 60 files)
FILE: plover_plugin/stenogotchi_link/clients.py
class StenogotchiClient (line 23) | class StenogotchiClient:
method __init__ (line 27) | def __init__(self, engineserver):
method _setup_dbus_loop (line 32) | def _setup_dbus_loop(self):
method _setup_object (line 38) | def _setup_object(self):
method _exit (line 52) | def _exit(self):
method plover_is_running (line 55) | def plover_is_running(self, b):
method plover_is_ready (line 61) | def plover_is_ready(self, b):
method plover_machine_state (line 64) | def plover_machine_state(self, s):
method plover_output_enabled (line 67) | def plover_output_enabled(self, b):
method plover_wpm_stats (line 70) | def plover_wpm_stats(self, s):
method plover_strokes_stats (line 73) | def plover_strokes_stats(self, s):
method send_backspaces (line 76) | def send_backspaces(self, y):
method send_string (line 79) | def send_string(self, s):
method send_key_combination (line 82) | def send_key_combination(self, s):
method send_lookup_results (line 85) | def send_lookup_results(self, l):
method stenogotchi_signal_handler (line 88) | def stenogotchi_signal_handler(self, dict):
class CustomKeyboardEmulation (line 115) | class CustomKeyboardEmulation(KeyboardEmulation):
method _update_keymap (line 121) | def _update_keymap(self):
class BTClient (line 175) | class BTClient:
method __init__ (line 179) | def __init__(self):
method update_mod_keys (line 190) | def update_mod_keys(self, mod_key, value):
method update_keys (line 204) | def update_keys(self, norm_key, value):
method state (line 222) | def state(self):
method clear_mod_keys (line 229) | def clear_mod_keys(self):
method clear_keys (line 232) | def clear_keys(self):
method send_keys (line 235) | def send_keys(self, state_list=None):
method send_backspaces (line 242) | def send_backspaces(self, number_of_backspaces):
method map_hid_events (line 254) | def map_hid_events(self, keycode, modifiers=None):
method send_string (line 308) | def send_string(self, s):
method send_key_combination (line 322) | def send_key_combination(self, combo_string: str):
FILE: plover_plugin/stenogotchi_link/keymap.py
function plover_convert (line 136) | def plover_convert(plover_keycode):
function plover_modkey (line 142) | def plover_modkey(plover_keycode):
FILE: plover_plugin/stenogotchi_link/stenogotchi_link.py
class EngineServer (line 24) | class EngineServer():
method __init__ (line 30) | def __init__(self, engine: StenoEngine) -> None:
method start (line 39) | def start(self):
method stop (line 46) | def stop(self):
method start_wpm_meter (line 51) | def start_wpm_meter(self, enable_wpm=False, enable_strokes=False, wpm_...
method stop_wpm_meter (line 59) | def stop_wpm_meter(self, disable_wpm=True, disable_strokes=True):
method _on_wpm_meter_update_strokes (line 67) | def _on_wpm_meter_update_strokes(self, stats):
method _on_wpm_meter_update_wpm (line 71) | def _on_wpm_meter_update_wpm(self, stats):
method _on_plover_translation (line 75) | def _on_plover_translation(self, results, type):
method lookup_word (line 98) | def lookup_word(self, word):
method lookup_stroke (line 102) | def lookup_stroke(self, stroke):
method get_server_status (line 106) | def get_server_status(self):
method _connect_hooks (line 113) | def _connect_hooks(self):
method _disconnect_hooks (line 124) | def _disconnect_hooks(self):
method _on_stroked (line 134) | def _on_stroked(self, stroke: Stroke):
method _on_translated (line 139) | def _on_translated(self, old: List[_Action], new: List[_Action]):
method _on_machine_state_changed (line 156) | def _on_machine_state_changed(self, machine_type: str, machine_state: ...
method _on_output_changed (line 166) | def _on_output_changed(self, enabled: bool):
method _on_config_changed (line 177) | def _on_config_changed(self, config_update: Config):
method _on_dictionaries_loaded (line 189) | def _on_dictionaries_loaded(self, dictionaries: StenoDictionaryCollect...
method _on_send_string (line 197) | def _on_send_string(self, text: str):
method _on_send_backspaces (line 207) | def _on_send_backspaces(self, count: int):
method _on_send_key_combination (line 217) | def _on_send_key_combination(self, combination: str):
method _on_add_translation (line 229) | def _on_add_translation(self):
method _on_focus (line 235) | def _on_focus(self):
method _on_configure (line 241) | def _on_configure(self):
method _on_lookup (line 247) | def _on_lookup(self):
method _on_suggestions (line 253) | def _on_suggestions(self):
method _on_quit (line 259) | def _on_quit(self):
FILE: plover_plugin/stenogotchi_link/wpm.py
class RepeatTimer (line 20) | class RepeatTimer(Timer):
method run (line 25) | def run(self):
class CaptureOutput (line 29) | class CaptureOutput(object):
method __init__ (line 31) | def __init__(self, chars):
method send_backspaces (line 34) | def send_backspaces(self, n):
method send_string (line 37) | def send_string(self, s):
method send_key_combination (line 40) | def send_key_combination(self, c):
method send_engine_command (line 43) | def send_engine_command(self, c):
class BaseMeter (line 47) | class BaseMeter():
method __init__ (line 49) | def __init__(self, timeout=60):
method on_translation (line 58) | def on_translation(self, old, new):
method on_timer (line 63) | def on_timer(self):
method trigger_event_update (line 66) | def trigger_event_update(self):
method quit (line 69) | def quit(self):
class PloverWpmMeter (line 73) | class PloverWpmMeter(BaseMeter):
method __init__ (line 75) | def __init__(self, stenogotchi_link, wpm_method='ncra', timeout=60):
method set_wpm_method (line 91) | def set_wpm_method(self, method):
method get_wpm_method (line 95) | def get_wpm_method(self):
method get_stats (line 100) | def get_stats(self):
method on_timer (line 103) | def on_timer(self):
method trigger_event_update (line 111) | def trigger_event_update(self):
class PloverStrokesMeter (line 115) | class PloverStrokesMeter(BaseMeter):
method __init__ (line 117) | def __init__(self, stenogotchi_link, strokes_method='ncra', timeout=60):
method set_strokes_method (line 138) | def set_strokes_method(self, method):
method get_strokes_method (line 142) | def get_strokes_method(self):
method get_stats (line 147) | def get_stats(self):
method on_translation (line 150) | def on_translation(self, old, new):
method on_timer (line 156) | def on_timer(self):
method trigger_event_update (line 170) | def trigger_event_update(self):
function _timestamp_items (line 173) | def _timestamp_items(items):
function _filter_old_items (line 178) | def _filter_old_items(items, timeout):
function _words_in_chars (line 184) | def _words_in_chars(chars, method):
function _time_interval_of_chars (line 203) | def _time_interval_of_chars(chars):
function _wpm_of_chars (line 211) | def _wpm_of_chars(chars, method):
function _spw_of_chars (line 222) | def _spw_of_chars(num_strokes, chars, method):
FILE: stenogotchi.py
function do_clear (line 18) | def do_clear(display):
function do_manual_mode (line 23) | def do_manual_mode(agent):
function do_auto_mode (line 35) | def do_auto_mode(agent):
function usr1_handler (line 117) | def usr1_handler(*unused):
FILE: stenogotchi/__init__.py
function set_name (line 13) | def set_name(new_name):
function name (line 46) | def name():
function uptime (line 54) | def uptime():
function mem_usage (line 59) | def mem_usage():
function _cpu_stat (line 77) | def _cpu_stat():
function cpu_load (line 84) | def cpu_load(s=0.1):
function temperature (line 99) | def temperature(celsius=True):
function get_wifi_status (line 105) | def get_wifi_status():
function get_wifi_ssid (line 115) | def get_wifi_ssid():
function set_wifi_onoff (line 122) | def set_wifi_onoff():
function shutdown (line 137) | def shutdown():
function restart (line 164) | def restart(mode):
function reboot (line 175) | def reboot(mode=None):
FILE: stenogotchi/agent.py
class Agent (line 19) | class Agent(Automata):
method __init__ (line 20) | def __init__(self, view, config):
method config (line 36) | def config(self):
method view (line 39) | def view(self):
method start (line 42) | def start(self):
method _update_uptime (line 49) | def _update_uptime(self):
method _update_wifi (line 53) | def _update_wifi(self):
method _reboot (line 79) | def _reboot(self):
method _save_recovery_data (line 84) | def _save_recovery_data(self):
method _load_recovery_data (line 93) | def _load_recovery_data(self, delete=True, no_exceptions=True):
method start_session_fetcher (line 109) | def start_session_fetcher(self):
method plover_status_update (line 112) | async def plover_status_update(self, msg):
method _fetch_stats (line 115) | def _fetch_stats(self):
method _on_event (line 122) | async def _on_event(self, msg): # no bettercap to produce events fo...
method _event_poller (line 125) | def _event_poller(self, loop):
method start_event_polling (line 130) | def start_event_polling(self):
FILE: stenogotchi/automata.py
class Automata (line 7) | class Automata(object):
method __init__ (line 8) | def __init__(self, config, view):
method set_starting (line 12) | def set_starting(self):
method set_ready (line 15) | def set_ready(self):
method in_good_mood (line 18) | def in_good_mood(self):
method set_grateful (line 22) | def set_grateful(self):
method set_lonely (line 26) | def set_lonely(self):
method set_bored (line 30) | def set_bored(self):
method set_sad (line 34) | def set_sad(self):
method set_angry (line 38) | def set_angry(self):
method set_excited (line 42) | def set_excited(self):
method set_processing (line 46) | def set_processing(self):
method set_rebooting (line 50) | def set_rebooting(self):
method wait_for (line 54) | def wait_for(self, t, sleeping=True):
method set_plover_boot (line 57) | def set_plover_boot(self):
method set_plover_ready (line 61) | def set_plover_ready(self):
method set_plover_quit (line 65) | def set_plover_quit(self):
method set_bt_connected (line 69) | def set_bt_connected(self, bthost_name):
method set_bt_disconnected (line 74) | def set_bt_disconnected(self):
method set_wifi_connected (line 79) | def set_wifi_connected(self, ssid, ip):
method set_wifi_disconnected (line 84) | def set_wifi_disconnected(self, ssid=''):
method set_wpm (line 89) | def set_wpm(self, wpm, wpm_top):
method set_strokes (line 94) | def set_strokes(self, stats):
method set_wpm_record (line 98) | def set_wpm_record(self, wpm_top):
method set_on_dict_lookup_done (line 103) | def set_on_dict_lookup_done(self):
FILE: stenogotchi/fs/__init__.py
function ensure_write (line 16) | def ensure_write(filename, mode='w'):
function size_of (line 28) | def size_of(path):
function is_mountpoint (line 39) | def is_mountpoint(path):
function setup_mounts (line 46) | def setup_mounts(config):
class MemoryFS (line 96) | class MemoryFS:
method zram_install (line 98) | def zram_install():
method zram_dev (line 106) | def zram_dev():
method __init__ (line 111) | def __init__(self, mount, disk, size="40M",
method _setup (line 126) | def _setup(self):
method daemonize (line 146) | def daemonize(self, interval=60):
method sync (line 153) | def sync(self, to_ram=False):
method mount (line 167) | def mount(self):
method umount (line 184) | def umount(self):
FILE: stenogotchi/log.py
class LastSession (line 17) | class LastSession(object):
method __init__ (line 21) | def __init__(self, config):
method _get_last_saved_session_id (line 31) | def _get_last_saved_session_id(self):
method save_session_id (line 40) | def save_session_id(self):
method _parse_datetime (line 45) | def _parse_datetime(self, dt):
method _parse_stats (line 51) | def _parse_stats(self):
method parse (line 91) | def parse(self, ui, skip=False):
method is_new (line 131) | def is_new(self):
function setup_logging (line 135) | def setup_logging(args, config):
function log_rotation (line 158) | def log_rotation(filename, cfg):
function parse_max_size (line 175) | def parse_max_size(s):
function do_rotate (line 194) | def do_rotate(filename, stats, cfg):
FILE: stenogotchi/plugins/__init__.py
class Plugin (line 16) | class Plugin:
method __init_subclass__ (line 18) | def __init_subclass__(cls, **kwargs):
function toggle_plugin (line 34) | def toggle_plugin(name, enable=True):
function on (line 78) | def on(event_name, *args, **kwargs):
function locked_cb (line 83) | def locked_cb(lock_name, cb, *args, **kwargs):
function one (line 93) | def one(plugin_name, event_name, *args, **kwargs):
function load_from_file (line 110) | def load_from_file(filename):
function load_from_path (line 119) | def load_from_path(path, enabled=()):
function load (line 135) | def load(config):
FILE: stenogotchi/plugins/cmd.py
function add_parsers (line 16) | def add_parsers(parser):
function used_plugin_cmd (line 63) | def used_plugin_cmd(args):
function handle_cmd (line 70) | def handle_cmd(args, config):
function edit (line 97) | def edit(args, config):
function enable (line 101) | def enable(args, config):
function disable (line 105) | def disable(args, config):
function upgrade (line 109) | def upgrade(args, config, pattern='*'):
function list_plugins (line 112) | def list_plugins(args, config, pattern='*'):
function _extract_version (line 121) | def _extract_version(filename):
function _get_available (line 128) | def _get_available():
function _get_installed (line 135) | def _get_installed(config):
function uninstall (line 149) | def uninstall(args, config):
function install (line 156) | def install(args, config):
function _analyse_dir (line 163) | def _analyse_dir(path):
function update (line 167) | def update(config):
FILE: stenogotchi/plugins/default/buttonshim.py
class Handler (line 82) | class Handler():
method __init__ (line 84) | def __init__(self, plugin):
class Buttonshim (line 100) | class Buttonshim(plugins.Plugin):
method __init__ (line 106) | def __init__(self):
method on_loaded (line 125) | def on_loaded(self):
method on_config_changed (line 133) | def on_config_changed(self, config):
method on_ready (line 136) | def on_ready(self, agent):
method set_ui_update (line 139) | def set_ui_update(self, key, value):
method trigger_ui_update (line 142) | def trigger_ui_update(self):
method _run (line 145) | def _run(self):
method _quit (line 208) | def _quit(self):
method setup (line 219) | def setup(self):
method _set_bit (line 241) | def _set_bit(self, pin, value):
method _next (line 248) | def _next(self):
method _enqueue (line 255) | def _enqueue(self):
method _chunk (line 261) | def _chunk(self, l, n):
method _write_byte (line 266) | def _write_byte(self, byte):
method on_hold (line 276) | def on_hold(self, buttons, handler=None, hold_time=1):
method on_press (line 313) | def on_press(self, buttons, handler=None, repeat=False, repeat_time=0.5):
method on_release (line 353) | def on_release(self, buttons=None, handler=None):
method set_brightness (line 389) | def set_brightness(self, brightness):
method set_pixel (line 401) | def set_pixel(self, r, g, b):
method blink (line 438) | def blink(self, r, g, b, ontime, offtime, blinktimes):
method press_handler (line 446) | def press_handler(self, button, pressed, plugin):
method toggle_qwerty_steno (line 450) | def toggle_qwerty_steno(self):
method toggle_wpm_meters (line 462) | def toggle_wpm_meters(self):
method toggle_dictionary_lookup (line 493) | def toggle_dictionary_lookup(self):
method hold_handler (line 502) | def hold_handler(self, button):
method release_handler (line 543) | def release_handler(self, button, pressed, plugin):
FILE: stenogotchi/plugins/default/dict_lookup.py
class UiHandler (line 13) | class UiHandler():
method __init__ (line 14) | def __init__(self, agent):
method store_state (line 23) | def store_state(self):
method restore_state (line 28) | def restore_state(self):
method surprise_exit (line 34) | def surprise_exit(self):
method relocate_face (line 43) | def relocate_face(self, minion):
method check_element (line 53) | def check_element(self, key):
method remove_element (line 56) | def remove_element(self, key):
method add_element (line 63) | def add_element(self, key, element):
method update_view (line 66) | def update_view(self):
method enable_input_mode (line 69) | def enable_input_mode(self):
method disable_input_mode (line 91) | def disable_input_mode(self):
method get_input_mode (line 117) | def get_input_mode(self):
method display_output (line 120) | def display_output(self, list):
method display_input (line 140) | def display_input(self, string, position_indicator="_"):
class InputHandler (line 147) | class InputHandler():
method __init__ (line 149) | def __init__(self, agent):
method _on_send_string (line 154) | def _on_send_string(self, text: str):
method _on_send_backspaces (line 165) | def _on_send_backspaces(self, count: int):
method _on_send_key_combination (line 172) | def _on_send_key_combination(self, combination: str):
method _on_lookup_results (line 178) | def _on_lookup_results(self, results_list):
method enable_input_mode (line 182) | def enable_input_mode(self):
method disable_input_mode (line 188) | def disable_input_mode(self):
method push_input (line 194) | def push_input(self, position_indicator="_"):
method clear_input (line 197) | def clear_input(self):
class DictLookup (line 202) | class DictLookup(plugins.Plugin):
method __init__ (line 208) | def __init__(self):
method on_plover_ready (line 216) | def on_plover_ready(self, agent):
method on_config_changed (line 222) | def on_config_changed(self, config):
method on_unload (line 225) | def on_unload(self, ui):
method get_running (line 229) | def get_running(self):
method get_input_mode (line 232) | def get_input_mode(self):
method enable_input_mode (line 235) | def enable_input_mode(self):
method disable_input_mode (line 240) | def disable_input_mode(self):
method lookup_word (line 246) | def lookup_word(self, word):
method lookup_stroke (line 252) | def lookup_stroke(self, stroke):
method display_lookup_result (line 260) | def display_lookup_result(self, results_list):
method sort_list (line 263) | def sort_list(self, rlist):
method push_input (line 275) | def push_input(self, string="", position_indicator="_"):
method push_output (line 279) | def push_output(self, rlist):
FILE: stenogotchi/plugins/default/evdevkb.py
class EvdevKbrd (line 203) | class EvdevKbrd:
method __init__ (line 208) | def __init__(self, skip_dbus = False):
method convert (line 227) | def convert(self, evdev_keycode):
method modkey (line 230) | def modkey(self, evdev_keycode):
method set_do_capture (line 236) | def set_do_capture(self, toggle):
method grab (line 239) | def grab(self):
method ungrab (line 244) | def ungrab(self):
method get_input_devices (line 249) | def get_input_devices(self):
method get_keyboards (line 254) | def get_keyboards(self):
method set_keyboards (line 266) | def set_keyboards(self):
method update_mod_keys (line 279) | def update_mod_keys(self, mod_key, value):
method update_keys (line 293) | def update_keys(self, norm_key, value):
method state (line 305) | def state(self):
method send_keys (line 313) | def send_keys(self):
method event_loop (line 321) | def event_loop(self):
class EvdevKeyboard (line 347) | class EvdevKeyboard(ObjectClass):
method __init__ (line 353) | def __init__(self):
method on_ready (line 358) | def on_ready(self, agent):
method on_config_changed (line 361) | def on_config_changed(self, config):
method trigger_ui_update (line 364) | def trigger_ui_update(self, input_mode):
method start_capture (line 368) | def start_capture(self):
method stop_capture (line 377) | def stop_capture(self):
method get_capture_state (line 384) | def get_capture_state(self):
FILE: stenogotchi/plugins/default/example.py
class Example (line 17) | class Example(plugins.Plugin):
method __init__ (line 23) | def __init__(self):
method on_webhook (line 29) | def on_webhook(self, path, request):
method on_loaded (line 33) | def on_loaded(self):
method on_unload (line 37) | def on_unload(self, ui):
method on_internet_available (line 41) | def on_internet_available(self, agent):
method on_ui_setup (line 45) | def on_ui_setup(self, ui):
method on_ui_update (line 51) | def on_ui_update(self, ui):
method on_display_setup (line 58) | def on_display_setup(self, display):
method on_ready (line 62) | def on_ready(self, agent):
method on_grateful (line 70) | def on_grateful(self, agent):
method on_lonely (line 74) | def on_lonely(self, agent):
method on_bored (line 78) | def on_bored(self, agent):
method on_sad (line 82) | def on_sad(self, agent):
method on_angry (line 86) | def on_angry(self, agent):
method on_excited (line 90) | def on_excited(self, agent):
method on_rebooting (line 94) | def on_rebooting(self, agent):
method on_wait (line 98) | def on_wait(self, agent, t):
method on_sleep (line 102) | def on_sleep(self, agent, t):
method on_epoch (line 106) | def on_epoch(self, agent, epoch, epoch_data):
method on_bt_connected (line 110) | def on_bt_connected(self, agent, bthost_name):
method on_bt_disconnected (line 114) | def on_bt_disconnected(self, agent):
method on_plover_boot (line 118) | def on_plover_boot(self, agent):
method on_plover_ready (line 122) | def on_plover_ready(self, agent):
method on_plover_quit (line 126) | def on_plover_quit(self, agent):
method on_wifi_connected (line 130) | def on_wifi_connected(self, agent, ssid, ip):
method on_wifi_disconnected (line 134) | def on_wifi_disconnected(self, agent):
method on_wpm_stats (line 138) | def on_wpm_stats(self, agent):
method on_strokes_stats (line 142) | def on_strokes_stats(self, agent):
FILE: stenogotchi/plugins/default/led.py
class Led (line 17) | class Led(plugins.Plugin):
method __init__ (line 23) | def __init__(self):
method on_loaded (line 31) | def on_loaded(self):
method on_config_changed (line 39) | def on_config_changed(self, config):
method _on_event (line 42) | def _on_event(self, event):
method _led (line 50) | def _led(self, on):
method _blink (line 54) | def _blink(self, pattern):
method _worker (line 65) | def _worker(self):
method on_updating (line 84) | def on_updating(self):
method on_internet_available (line 88) | def on_internet_available(self, agent):
method on_ready (line 92) | def on_ready(self, agent):
method on_grateful (line 96) | def on_grateful(self, agent):
method on_lonely (line 100) | def on_lonely(self, agent):
method on_bored (line 104) | def on_bored(self, agent):
method on_sad (line 108) | def on_sad(self, agent):
method on_angry (line 112) | def on_angry(self, agent):
method on_excited (line 116) | def on_excited(self, agent):
method on_rebooting (line 120) | def on_rebooting(self, agent):
method on_wait (line 124) | def on_wait(self, agent, t):
method on_sleep (line 128) | def on_sleep(self, agent, t):
method on_epoch (line 132) | def on_epoch(self, agent, epoch, epoch_data):
method on_bt_connected (line 136) | def on_bt_connected(self, agent, bthost_name):
method on_bt_disconnected (line 140) | def on_bt_disconnected(self, agent):
method on_plover_boot (line 144) | def on_plover_boot(self, agent):
method on_plover_ready (line 148) | def on_plover_ready(self, agent):
method on_plover_quit (line 152) | def on_plover_quit(self, agent):
method on_wifi_connected (line 156) | def on_wifi_connected(self, agent, ssid, ip):
method on_wifi_disconnected (line 160) | def on_wifi_disconnected(self, agent):
method on_wpm_stats (line 164) | def on_wpm_stats(self, agent):
method on_strokes_stats (line 168) | def on_strokes_stats(self, agent):
FILE: stenogotchi/plugins/default/logtail.py
class Logtail (line 235) | class Logtail(plugins.Plugin):
method __init__ (line 241) | def __init__(self):
method on_config_changed (line 246) | def on_config_changed(self, config):
method on_loaded (line 250) | def on_loaded(self):
method on_webhook (line 257) | def on_webhook(self, path, request):
FILE: stenogotchi/plugins/default/memtemp.py
class MemTemp (line 35) | class MemTemp(plugins.Plugin):
method __init__ (line 41) | def __init__(self):
method on_loaded (line 44) | def on_loaded(self):
method on_config_changed (line 48) | def on_config_changed(self, config):
method mem_usage (line 51) | def mem_usage(self):
method cpu_load (line 54) | def cpu_load(self):
method _cpu_poller (line 59) | def _cpu_poller(self, s=60):
method on_ui_setup (line 66) | def on_ui_setup(self, ui):
method on_unload (line 96) | def on_unload(self, ui):
method on_ui_update (line 100) | def on_ui_update(self, ui):
FILE: stenogotchi/plugins/default/plover_link.py
class BluezErrorRejected (line 30) | class BluezErrorRejected(dbus.DBusException):
class BluezErrorCanceled (line 34) | class BluezErrorCanceled(dbus.DBusException):
class Agent (line 38) | class Agent(dbus.service.Object):
method AuthorizeService (line 47) | def AuthorizeService(self, device, uuid):
method RequestAuthorization (line 53) | def RequestAuthorization(self, device):
method Cancel (line 59) | def Cancel(self):
method Release (line 65) | def Release(self):
class HumanInterfaceDeviceProfile (line 70) | class HumanInterfaceDeviceProfile(dbus.service.Object):
method Release (line 78) | def Release(self):
method NewConnection (line 84) | def NewConnection(self, path, fd, properties):
method RequestDisconnection (line 96) | def RequestDisconnection(self, path):
class BTKbDevice (line 104) | class BTKbDevice:
method __init__ (line 128) | def __init__(self, hci=0):
method interfaces_added (line 178) | def interfaces_added(self, path, device_info):
method _properties_changed (line 181) | def _properties_changed(self, interface, changed, invalidated, path):
method on_disconnect (line 187) | def on_disconnect(self):
method address (line 205) | def address(self):
method powered (line 211) | def powered(self):
method powered (line 216) | def powered(self, new_state):
method alias (line 220) | def alias(self):
method alias (line 225) | def alias(self, new_alias):
method discoverabletimeout (line 231) | def discoverabletimeout(self):
method discoverabletimeout (line 237) | def discoverabletimeout(self, new_timeout):
method discoverable (line 243) | def discoverable(self):
method discoverable (line 249) | def discoverable(self, new_state):
method register_hid_profile (line 254) | def register_hid_profile(self):
method register_bt_pairing_agent (line 283) | def register_bt_pairing_agent(self):
method read_sdp_service_record (line 300) | def read_sdp_service_record():
method create_ssockets (line 313) | def create_ssockets(self):
method listen (line 331) | def listen(self):
method auto_connect (line 389) | def auto_connect(self):
method unpair_device (line 467) | def unpair_device(self, address):
method get_connected_device_name (line 480) | def get_connected_device_name(self):
method send (line 495) | def send(self, msg):
class StenogotchiService (line 503) | class StenogotchiService(dbus.service.Object):
method __init__ (line 509) | def __init__(self):
method auto_connect (line 518) | def auto_connect(self):
method send_keys (line 527) | def send_keys(self, key_list):
method plover_is_running (line 532) | def plover_is_running(self, b):
method plover_is_ready (line 540) | def plover_is_ready(self, b):
method plover_machine_state (line 545) | def plover_machine_state(self, s):
method plover_output_enabled (line 549) | def plover_output_enabled(self, b):
method plover_wpm_stats (line 553) | def plover_wpm_stats(self, s):
method plover_strokes_stats (line 568) | def plover_strokes_stats(self, s):
method send_string_stenogotchi (line 573) | def send_string_stenogotchi(self, s):
method send_backspaces_stenogotchi (line 578) | def send_backspaces_stenogotchi(self, n):
method send_key_combination_stenogotchi (line 583) | def send_key_combination_stenogotchi(self, s):
method plover_translation_handler (line 588) | def plover_translation_handler(self, l):
method signal_to_plover (line 593) | def signal_to_plover(self, message):
class PloverLink (line 597) | class PloverLink(ObjectClass):
method __init__ (line 603) | def __init__(self):
method on_ready (line 610) | def on_ready(self, agent):
method on_plover_ready (line 624) | def on_plover_ready(self, agent):
method on_config_changed (line 627) | def on_config_changed(self, config):
method on_unload (line 630) | def on_unload(self, ui):
method send_signal_to_plover (line 633) | def send_signal_to_plover(self, message):
FILE: stenogotchi/plugins/default/upslite.py
class Upslite (line 27) | class Upslite(ObjectClass):
method __init__ (line 33) | def __init__(self):
method on_loaded (line 42) | def on_loaded(self):
method on_config_changed (line 55) | def on_config_changed(self, config):
method on_ui_update (line 59) | def on_ui_update(self, ui):
method _read_voltage (line 84) | def _read_voltage(self):
method _read_charge (line 92) | def _read_charge(self):
method _check_plugged (line 105) | def _check_plugged(self):
method _quickstart (line 111) | def _quickstart(self):
method _power_on_reset (line 114) | def _power_on_reset(self):
method get_charge (line 117) | def get_charge(self):
method get_voltage (line 121) | def get_voltage(self):
method get_is_plugged (line 125) | def get_is_plugged(self):
FILE: stenogotchi/ui/components.py
class Widget (line 5) | class Widget(object):
method __init__ (line 6) | def __init__(self, xy, color=0):
method draw (line 10) | def draw(self, canvas, drawer):
class Bitmap (line 14) | class Bitmap(Widget):
method __init__ (line 15) | def __init__(self, path, xy, color=0):
method draw (line 19) | def draw(self, canvas, drawer):
class Line (line 23) | class Line(Widget):
method __init__ (line 24) | def __init__(self, xy, color=0, width=1):
method draw (line 28) | def draw(self, canvas, drawer):
class Rect (line 32) | class Rect(Widget):
method draw (line 33) | def draw(self, canvas, drawer):
class FilledRect (line 37) | class FilledRect(Widget):
method draw (line 38) | def draw(self, canvas, drawer):
class Text (line 42) | class Text(Widget):
method __init__ (line 43) | def __init__(self, value="", position=(0, 0), font=None, color=0, wrap...
method draw (line 51) | def draw(self, canvas, drawer):
class LabeledValue (line 60) | class LabeledValue(Widget):
method __init__ (line 61) | def __init__(self, label, value="", position=(0, 0), label_font=None, ...
method draw (line 70) | def draw(self, canvas, drawer):
FILE: stenogotchi/ui/display.py
class Display (line 10) | class Display(View):
method __init__ (line 11) | def __init__(self, config, state={}):
method is_waveshare_v2 (line 28) | def is_waveshare_v2(self):
method init_display (line 31) | def init_display(self):
method clear (line 39) | def clear(self):
method image (line 42) | def image(self):
method _render_thread (line 48) | def _render_thread(self):
method _on_view_rendered (line 56) | def _on_view_rendered(self, img):
FILE: stenogotchi/ui/faces.py
function load_from_config (line 29) | def load_from_config(config):
FILE: stenogotchi/ui/fonts.py
function init (line 18) | def init(config):
function status_font (line 25) | def status_font(old_font):
function setup (line 30) | def setup(bold, bold_small, medium, huge, bold_big, small):
FILE: stenogotchi/ui/hw/__init__.py
function display_for (line 3) | def display_for(config):
FILE: stenogotchi/ui/hw/base.py
class DisplayImpl (line 4) | class DisplayImpl(object):
method __init__ (line 5) | def __init__(self, config, name):
method layout (line 38) | def layout(self):
method initialize (line 41) | def initialize(self):
method render (line 44) | def render(self, canvas):
method clear (line 47) | def clear(self):
FILE: stenogotchi/ui/hw/libs/waveshare/v2/epd2in13_V2.py
function digital_write (line 63) | def digital_write(pin, value):
function digital_read (line 67) | def digital_read(pin):
function delay_ms (line 71) | def delay_ms(delaytime):
function spi_writebyte (line 75) | def spi_writebyte(data):
function module_init (line 79) | def module_init():
class EPD (line 96) | class EPD:
method __init__ (line 97) | def __init__(self):
method reset (line 143) | def reset(self):
method send_command (line 151) | def send_command(self, command):
method send_data (line 155) | def send_data(self, data):
method wait_until_idle (line 159) | def wait_until_idle(self):
method TurnOnDisplay (line 163) | def TurnOnDisplay(self):
method init (line 169) | def init(self, update):
method getbuffer (line 259) | def getbuffer(self, image):
method display (line 288) | def display(self, image):
method displayPartial (line 300) | def displayPartial(self, image):
method Clear (line 316) | def Clear(self, color):
method sleep (line 329) | def sleep(self):
FILE: stenogotchi/ui/hw/waveshare2.py
class WaveshareV2 (line 7) | class WaveshareV2(DisplayImpl):
method __init__ (line 8) | def __init__(self, config):
method layout (line 12) | def layout(self):
method initialize (line 84) | def initialize(self):
method render (line 92) | def render(self, canvas):
method clear (line 96) | def clear(self):
FILE: stenogotchi/ui/state.py
class State (line 4) | class State(object):
method __init__ (line 5) | def __init__(self, state={}):
method add_element (line 11) | def add_element(self, key, elem):
method has_element (line 15) | def has_element(self, key):
method remove_element (line 18) | def remove_element(self, key):
method add_listener (line 22) | def add_listener(self, key, cb):
method items (line 26) | def items(self):
method get (line 30) | def get(self, key):
method reset (line 34) | def reset(self):
method changes (line 38) | def changes(self, ignore=()):
method has_changes (line 46) | def has_changes(self):
method set (line 50) | def set(self, key, value):
FILE: stenogotchi/ui/view.py
class View (line 24) | class View(object):
method __init__ (line 25) | def __init__(self, config, impl, state=None):
method set_agent (line 102) | def set_agent(self, agent):
method has_element (line 105) | def has_element(self, key):
method add_element (line 108) | def add_element(self, key, elem):
method remove_element (line 111) | def remove_element(self, key):
method width (line 114) | def width(self):
method height (line 117) | def height(self):
method on_state_change (line 120) | def on_state_change(self, key, cb):
method on_render (line 123) | def on_render(self, cb):
method _refresh_handler (line 127) | def _refresh_handler(self):
method set (line 139) | def set(self, key, value):
method get (line 142) | def get(self, key):
method on_starting (line 145) | def on_starting(self):
method on_manual_mode (line 150) | def on_manual_mode(self, last_session):
method is_normal (line 157) | def is_normal(self):
method on_keys_generation (line 170) | def on_keys_generation(self):
method on_normal (line 175) | def on_normal(self):
method on_reading_logs (line 180) | def on_reading_logs(self, lines_so_far=0):
method wait (line 185) | def wait(self, secs, sleeping=True):
method on_shutdown (line 215) | def on_shutdown(self):
method on_bored (line 222) | def on_bored(self):
method on_sad (line 227) | def on_sad(self):
method on_angry (line 232) | def on_angry(self):
method on_motivated (line 237) | def on_motivated(self, reward):
method on_demotivated (line 242) | def on_demotivated(self, reward):
method on_excited (line 247) | def on_excited(self):
method on_miss (line 252) | def on_miss(self, who):
method on_grateful (line 257) | def on_grateful(self):
method on_lonely (line 262) | def on_lonely(self):
method on_processing (line 267) | def on_processing(self):
method on_rebooting (line 277) | def on_rebooting(self):
method on_custom (line 282) | def on_custom(self, text):
method on_plover_boot (line 287) | def on_plover_boot(self):
method on_plover_ready (line 293) | def on_plover_ready(self):
method on_plover_quit (line 301) | def on_plover_quit(self):
method on_wpm (line 309) | def on_wpm(self, wpm):
method on_strokes (line 313) | def on_strokes(self, strokes):
method on_wpm_record (line 317) | def on_wpm_record(self, wpm_top):
method on_bt_connected (line 327) | def on_bt_connected(self, bthost_name):
method on_bt_disconnected (line 332) | def on_bt_disconnected(self):
method on_wifi_connected (line 337) | def on_wifi_connected(self, ssid, ip):
method on_wifi_disconnected (line 342) | def on_wifi_disconnected(self):
method on_dict_lookup_done (line 347) | def on_dict_lookup_done(self):
method update (line 353) | def update(self, force=False, new_data={}):
FILE: stenogotchi/ui/web/__init__.py
function update_frame (line 10) | def update_frame(img):
FILE: stenogotchi/ui/web/handler.py
class Handler (line 24) | class Handler:
method __init__ (line 25) | def __init__(self, config, agent, app):
method _check_creds (line 51) | def _check_creds(self, u, p):
method with_auth (line 56) | def with_auth(self, f):
method index (line 67) | def index(self):
method plugins (line 72) | def plugins(self, name, subpath):
method shutdown (line 89) | def shutdown(self):
method toggle_input (line 97) | def toggle_input(self):
method toggle_wpm (line 106) | def toggle_wpm(self):
method toggle_lookup (line 115) | def toggle_lookup(self):
method buttonshim (line 128) | def buttonshim(self, button):
method reboot (line 147) | def reboot(self):
method restart (line 155) | def restart(self):
method ui (line 167) | def ui(self):
FILE: stenogotchi/ui/web/server.py
class Server (line 16) | class Server:
method __init__ (line 17) | def __init__(self, agent, config):
method _http_serve (line 30) | def _http_serve(self):
FILE: stenogotchi/ui/web/static/js/jquery.jqplot.js
function Axis (line 537) | function Axis(name) {
function Legend (line 873) | function Legend(options) {
function Title (line 1105) | function Title(text) {
function Series (line 1165) | function Series(options) {
function Grid (line 1550) | function Grid() {
function jqPlot (line 1742) | function jqPlot() {
function drawLine (line 4688) | function drawLine(bx, by, ex, ey, opts) {
function getSteps (line 5315) | function getSteps (d, f) {
function computeSteps (line 5319) | function computeSteps (d1, d2) {
function tanh (line 5324) | function tanh (x) {
function computeConstrainedSmoothedData (line 5343) | function computeConstrainedSmoothedData (gd) {
function computeHermiteSmoothedData (line 5463) | function computeHermiteSmoothedData (gd) {
function postInit (line 5992) | function postInit(target, data, options) {
function postPlotDraw (line 6006) | function postPlotDraw() {
function highlight (line 6021) | function highlight (plot, sidx, pidx, points) {
function unhighlight (line 6036) | function unhighlight (plot) {
function handleMove (line 6048) | function handleMove(ev, gridpos, datapos, neighbor, plot) {
function handleMouseDown (line 6069) | function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
function handleMouseUp (line 6086) | function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
function handleClick (line 6093) | function handleClick(ev, gridpos, datapos, neighbor, plot) {
function handleRightClick (line 6104) | function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
function bestFormatString (line 7117) | function bestFormatString (interval)
function bestConstrainedInterval (line 7167) | function bestConstrainedInterval(min, max, nttarget) {
function bestInterval (line 7218) | function bestInterval(range, numberTicks) {
function bestLinearInterval (line 7268) | function bestLinearInterval(range, scalefact) {
function bestLinearComponents (line 7303) | function bestLinearComponents(range, scalefact) {
function numericalOrder (line 8508) | function numericalOrder(a,b) { return a-b; }
function clone (line 8776) | function clone(obj){
function merge (line 8790) | function merge(obj1, obj2) {
function getLineheight (line 9178) | function getLineheight(el) {
function writeWrappedText (line 9187) | function writeWrappedText (el, context, text, left, top, canvasWidth) {
function _jqpToImage (line 9240) | function _jqpToImage(el, x_offset, y_offset) {
function h1 (line 10655) | function h1(parsable, match) {
function inArray (line 10833) | function inArray( elem, array ) {
function get_type (line 10850) | function get_type(thing){
function pad (line 10921) | function pad(str, len, chr, leftJustify) {
function thousand_separate (line 10927) | function thousand_separate(value) {
function justify (line 10935) | function justify(value, prefix, leftJustify, minWidth, zeroPad, htmlSpac...
function formatBaseX (line 10949) | function formatBaseX(value, base, prefix, leftJustify, minWidth, precisi...
function formatString (line 10957) | function formatString(value, leftJustify, minWidth, precision, zeroPad, ...
function _normalizeArguments (line 11281) | function _normalizeArguments( effect, options, speed, callback ) {
function standardSpeed (line 11330) | function standardSpeed( speed ) {
function run (line 11371) | function run( next ) {
FILE: stenogotchi/ui/web/static/js/jquery.mobile/jquery.mobile-1.4.5.js
function focusable (line 121) | function focusable( element, isTabIndexNotNaN ) {
function visible (line 142) | function visible( element ) {
function reduce (line 184) | function reduce( elem, size, border, margin ) {
function handlerProxy (line 1128) | function handlerProxy() {
function handlerProxy (line 1164) | function handlerProxy() {
function get_fragment (line 1650) | function get_fragment( url ) {
function poll (line 1824) | function poll() {
function propExists (line 1996) | function propExists( prop ) {
function inlineSVG (line 2017) | function inlineSVG() {
function transform3dTest (line 2037) | function transform3dTest() {
function baseTagTest (line 2066) | function baseTagTest() {
function cssPointerEventsTest (line 2090) | function cssPointerEventsTest() {
function boundingRect (line 2109) | function boundingRect() {
function fixedPosition (line 2129) | function fixedPosition() {
function getNativeEvent (line 3358) | function getNativeEvent( event ) {
function createVirtualEvent (line 3366) | function createVirtualEvent( event, eventType ) {
function getVirtualBindingFlags (line 3416) | function getVirtualBindingFlags( element ) {
function getClosestElementWithVirtualBinding (line 3435) | function getClosestElementWithVirtualBinding( element, eventType ) {
function enableTouchBindings (line 3449) | function enableTouchBindings() {
function disableTouchBindings (line 3453) | function disableTouchBindings() {
function enableMouseBindings (line 3457) | function enableMouseBindings() {
function disableMouseBindings (line 3467) | function disableMouseBindings() {
function startResetTimer (line 3473) | function startResetTimer() {
function clearResetTimer (line 3481) | function clearResetTimer() {
function triggerVirtualEvent (line 3488) | function triggerVirtualEvent( eventType, event, flags ) {
function mouseEventCallback (line 3502) | function mouseEventCallback( event ) {
function handleTouchStart (line 3522) | function handleTouchStart( event ) {
function handleScroll (line 3552) | function handleScroll( event ) {
function handleTouchMove (line 3565) | function handleTouchMove( event ) {
function handleTouchEnd (line 3587) | function handleTouchEnd( event ) {
function hasVirtualBindings (line 3623) | function hasVirtualBindings( ele ) {
function dummyMouseHandler (line 3637) | function dummyMouseHandler() {}
function getSpecialEventObject (line 3639) | function getSpecialEventObject( eventType ) {
function triggerCustomEvent (line 3843) | function triggerCustomEvent( obj, eventType, event, bubble ) {
function trigger (line 3865) | function trigger( event, state ) {
function clearTapTimer (line 3910) | function clearTapTimer() {
function clearTapHandlers (line 3914) | function clearTapHandlers() {
function clickHandler (line 3922) | function clickHandler( event ) {
function handler (line 4279) | function handler() {
function findClosestLink (line 5760) | function findClosestLink( ele ) {
function noHiddenClass (line 7178) | function noHiddenClass( elements ) {
function defaultAutodividersSelector (line 7663) | function defaultAutodividersSelector( elt ) {
function getPopup (line 9080) | function getPopup() {
function fitSegmentInsideSegment (line 10283) | function fitSegmentInsideSegment( windowSize, segmentSize, offset, desir...
function getWindowCoordinates (line 10297) | function getWindowCoordinates( theWindow ) {
function optionsToClasses (line 11848) | function optionsToClasses( options, existingClasses ) {
function classNameToOptions (line 11905) | function classNameToOptions( classes ) {
function camelCase2Hyphenated (line 11973) | function camelCase2Hyphenated( c ) {
function getArrow (line 12837) | function getArrow() {
function getNextTabId (line 14415) | function getNextTabId() {
function isLocal (line 14419) | function isLocal( anchor ) {
function constrain (line 14635) | function constrain() {
function complete (line 14981) | function complete() {
function show (line 14986) | function show() {
function checkTilt (line 15261) | function checkTilt( e ) {
function hideRenderingClass (line 15294) | function hideRenderingClass() {
FILE: stenogotchi/ui/web/static/js/jquery.timeago.js
function substitute (line 96) | function substitute(stringOrFunction, number) {
function refresh (line 183) | function refresh() {
function prepareData (line 207) | function prepareData(element) {
function inWords (line 221) | function inWords(date) {
function distance (line 225) | function distance(date) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.BezierCurveRenderer.js
function preInit (line 289) | function preInit(target, data, options) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.barRenderer.js
function barPreInit (line 176) | function barPreInit(target, data, seriesDefaults, options) {
function computeHighlightColors (line 279) | function computeHighlightColors (colors) {
function getStart (line 295) | function getStart(sidx, didx, comp, plot, axis) {
function postInit (line 675) | function postInit(target, data, options) {
function postPlotDraw (line 689) | function postPlotDraw() {
function highlight (line 705) | function highlight (plot, sidx, pidx, points) {
function unhighlight (line 716) | function unhighlight (plot) {
function handleMove (line 728) | function handleMove(ev, gridpos, datapos, neighbor, plot) {
function handleMouseDown (line 750) | function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
function handleMouseUp (line 767) | function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
function handleClick (line 774) | function handleClick(ev, gridpos, datapos, neighbor, plot) {
function handleRightClick (line 785) | function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.bubbleRenderer.js
function highlight (line 581) | function highlight (plot, sidx, pidx) {
function unhighlight (line 611) | function unhighlight (plot) {
function handleMove (line 624) | function handleMove(ev, gridpos, datapos, neighbor, plot) {
function handleMouseDown (line 647) | function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
function handleMouseUp (line 666) | function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
function handleClick (line 673) | function handleClick(ev, gridpos, datapos, neighbor, plot) {
function handleRightClick (line 686) | function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
function postPlotDraw (line 706) | function postPlotDraw() {
function preInit (line 732) | function preInit(target, data, options) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.canvasOverlay.js
function LineBase (line 89) | function LineBase() {
function Rectangle (line 161) | function Rectangle(options) {
function Line (line 198) | function Line(options) {
function HorizontalLine (line 224) | function HorizontalLine(options) {
function DashedHorizontalLine (line 258) | function DashedHorizontalLine(options) {
function VerticalLine (line 289) | function VerticalLine(options) {
function DashedVerticalLine (line 315) | function DashedVerticalLine(options) {
function showTooltip (line 782) | function showTooltip(plot, obj, gridpos, datapos) {
function isNearLine (line 842) | function isNearLine(point, lstart, lstop, width) {
function isNearRectangle (line 860) | function isNearRectangle(point, lstart, lstop, width) {
function handleMove (line 879) | function handleMove(ev, gridpos, datapos, neighbor, plot) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.ciParser.js
function handleStrings (line 83) | function handleStrings(key, value) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.cursor.js
function plotZoom (line 308) | function plotZoom(ev, gridpos, datapos, plot, cursor) {
function plotReset (line 312) | function plotReset(ev, plot, cursor) {
function updateTooltip (line 468) | function updateTooltip(gridpos, datapos, plot) {
function moveLine (line 539) | function moveLine(gridpos, plot) {
function getIntersectingPoints (line 585) | function getIntersectingPoints(plot, x, y) {
function moveTooltip (line 613) | function moveTooltip(gridpos, plot) {
function positionTooltip (line 660) | function positionTooltip(plot) {
function handleClick (line 717) | function handleClick (ev, gridpos, datapos, neighbor, plot) {
function handleDblClick (line 735) | function handleDblClick (ev, gridpos, datapos, neighbor, plot) {
function handleMouseLeave (line 753) | function handleMouseLeave(ev, gridpos, datapos, neighbor, plot) {
function handleMouseEnter (line 789) | function handleMouseEnter(ev, gridpos, datapos, neighbor, plot) {
function handleMouseMove (line 812) | function handleMouseMove(ev, gridpos, datapos, neighbor, plot) {
function getEventPosition (line 827) | function getEventPosition(ev) {
function handleZoomMove (line 848) | function handleZoomMove(ev) {
function handleMouseDown (line 893) | function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
function handleMouseUp (line 942) | function handleMouseUp(ev) {
function drawZoomBox (line 1002) | function drawZoomBox() {
function addrow (line 1085) | function addrow(label, color, pad, idx) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.dateAxisRenderer.js
function bestDateInterval (line 131) | function bestDateInterval(min, max, titarget) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.donutRenderer.js
function doDraw (line 294) | function doDraw () {
function preInit (line 630) | function preInit(target, data, options) {
function postInit (line 657) | function postInit(target, data, options) {
function postParseOptions (line 682) | function postParseOptions(options) {
function highlight (line 689) | function highlight (plot, sidx, pidx) {
function unhighlight (line 698) | function unhighlight (plot) {
function handleMove (line 708) | function handleMove(ev, gridpos, datapos, neighbor, plot) {
function handleMouseDown (line 729) | function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
function handleMouseUp (line 746) | function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
function handleClick (line 753) | function handleClick(ev, gridpos, datapos, neighbor, plot) {
function handleRightClick (line 764) | function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
function postPlotDraw (line 782) | function postPlotDraw() {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.dragable.js
function DragCanvas (line 57) | function DragCanvas() {
function initDragPoint (line 104) | function initDragPoint(plot, neighbor) {
function handleMove (line 128) | function handleMove(ev, gridpos, datapos, neighbor, plot) {
function handleDown (line 178) | function handleDown(ev, gridpos, datapos, neighbor, plot) {
function handleUp (line 201) | function handleUp(ev, gridpos, datapos, neighbor, plot) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.funnelRenderer.js
function doDraw (line 272) | function doDraw () {
function findleft (line 396) | function findleft (l) {
function findright (line 404) | function findright (l) {
function preInit (line 768) | function preInit(target, data, options) {
function postInit (line 795) | function postInit(target, data, options) {
function postParseOptions (line 809) | function postParseOptions(options) {
function highlight (line 816) | function highlight (plot, sidx, pidx) {
function unhighlight (line 825) | function unhighlight (plot) {
function handleMove (line 835) | function handleMove(ev, gridpos, datapos, neighbor, plot) {
function handleMouseDown (line 856) | function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
function handleMouseUp (line 873) | function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
function handleClick (line 880) | function handleClick(ev, gridpos, datapos, neighbor, plot) {
function handleRightClick (line 891) | function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
function postPlotDraw (line 909) | function postPlotDraw() {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.highlighter.js
function draw (line 213) | function draw(plot, neighbor) {
function showTooltip (line 229) | function showTooltip(plot, series, neighbor) {
function handleMove (line 399) | function handleMove(ev, gridpos, datapos, neighbor, plot) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.json2.js
function f (line 164) | function f(n) {
function quote (line 205) | function quote(string) {
function str (line 223) | function str(key, holder) {
function walk (line 406) | function walk(holder, key) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.logAxisRenderer.js
function findCeil (line 170) | function findCeil (val) {
function findFloor (line 175) | function findFloor(val) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.mekkoRenderer.js
function preInit (line 411) | function preInit(target, data, options) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.meterGaugeRenderer.js
function getnmt (line 304) | function getnmt(pos, interval, fact) {
function preInit (line 980) | function preInit(target, data, options) {
function postParseOptions (line 1013) | function postParseOptions(options) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.mobile.js
function postInit (line 23) | function postInit(target, data, options){
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.pieRenderer.js
function calcRadiusAdjustment (line 266) | function calcRadiusAdjustment(ang) {
function calcRPrime (line 270) | function calcRPrime(ang1, ang2, sliceMargin, fill, lineWidth) {
function doDraw (line 329) | function doDraw (rad) {
function preInit (line 771) | function preInit(target, data, options) {
function postInit (line 797) | function postInit(target, data, options) {
function postParseOptions (line 809) | function postParseOptions(options) {
function highlight (line 816) | function highlight (plot, sidx, pidx) {
function unhighlight (line 827) | function unhighlight (plot) {
function handleMove (line 837) | function handleMove(ev, gridpos, datapos, neighbor, plot) {
function handleMouseDown (line 858) | function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
function handleMouseUp (line 875) | function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
function handleClick (line 882) | function handleClick(ev, gridpos, datapos, neighbor, plot) {
function handleRightClick (line 893) | function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
function postPlotDraw (line 911) | function postPlotDraw() {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.pyramidGridRenderer.js
function drawLine (line 374) | function drawLine(bx, by, ex, ey, opts) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.pyramidRenderer.js
function preInit (line 416) | function preInit(target, data, options) {
function postPlotDraw (line 445) | function postPlotDraw() {
function highlight (line 461) | function highlight (plot, sidx, pidx, points) {
function unhighlight (line 477) | function unhighlight (plot) {
function handleMove (line 489) | function handleMove(ev, gridpos, datapos, neighbor, plot) {
FILE: stenogotchi/ui/web/static/js/plugins/jqplot.trendline.js
function addTrendlineLegend (line 95) | function addTrendlineLegend(series) {
function parseTrendLineOptions (line 107) | function parseTrendLineOptions (target, data, seriesDefaults, options, p...
function drawTrendline (line 117) | function drawTrendline(sctx, options) {
function regression (line 131) | function regression(x, y, typ) {
function linearRegression (line 175) | function linearRegression(X,Y) {
function expRegression (line 181) | function expRegression(X,Y) {
function fitData (line 191) | function fitData(data, typ) {
FILE: stenogotchi/ui/web/static/js/viewportHeight.js
function updateViewportSize (line 5) | function updateViewportSize() {
FILE: stenogotchi/utils.py
class DottedTomlEncoder (line 18) | class DottedTomlEncoder(TomlEncoder):
method __init__ (line 23) | def __init__(self, _dict=dict):
method dump_list (line 26) | def dump_list(self, v):
method dump_sections (line 39) | def dump_sections(self, o, sup):
function parse_version (line 63) | def parse_version(version):
function download_file (line 69) | def download_file(url, destination, chunk_size=128):
function unzip (line 78) | def unzip(file, destination, strip_dirs=0):
function merge_config (line 91) | def merge_config(user, default):
function load_configOLD (line 100) | def load_configOLD(args):
function save_config (line 117) | def save_config(config, target):
function load_config (line 122) | def load_config(args):
function secs_to_hhmmss (line 240) | def secs_to_hhmmss(secs):
function led (line 245) | def led(on=True):
function blink (line 250) | def blink(times=1, delay=0.3):
class StatusFile (line 258) | class StatusFile(object):
method __init__ (line 259) | def __init__(self, path, data_format='raw'):
method data_field_or (line 273) | def data_field_or(self, name, default=""):
method newer_then_minutes (line 278) | def newer_then_minutes(self, minutes):
method newer_then_hours (line 281) | def newer_then_hours(self, hours):
method newer_then_days (line 284) | def newer_then_days(self, days):
method update (line 287) | def update(self, data=None):
FILE: stenogotchi/voice.py
class Voice (line 6) | class Voice:
method __init__ (line 7) | def __init__(self, lang):
method custom (line 17) | def custom(self, s):
method default (line 20) | def default(self):
method on_starting (line 23) | def on_starting(self):
method on_ai_ready (line 29) | def on_ai_ready(self):
method on_keys_generation (line 34) | def on_keys_generation(self):
method on_normal (line 38) | def on_normal(self):
method on_reading_logs (line 53) | def on_reading_logs(self, lines_so_far=0):
method on_bored (line 59) | def on_bored(self):
method on_motivated (line 65) | def on_motivated(self, reward):
method on_demotivated (line 68) | def on_demotivated(self, reward):
method on_sad (line 76) | def on_sad(self):
method on_angry (line 83) | def on_angry(self):
method on_excited (line 90) | def on_excited(self):
method on_new_peer (line 98) | def on_new_peer(self, peer):
method on_lost_peer (line 108) | def on_lost_peer(self, peer):
method on_miss (line 113) | def on_miss(self, who):
method on_grateful (line 119) | def on_grateful(self):
method on_lonely (line 124) | def on_lonely(self):
method on_napping (line 130) | def on_napping(self, secs):
method on_shutdown (line 137) | def on_shutdown(self):
method on_awakening (line 142) | def on_awakening(self):
method on_waiting (line 147) | def on_waiting(self, secs):
method on_rebooting (line 153) | def on_rebooting(self):
method on_last_session_data (line 156) | def on_last_session_data(self, last_session):
method on_last_session_tweet (line 160) | def on_last_session_tweet(self, last_session):
method on_bt_connected (line 163) | def on_bt_connected(self, bthost_name):
method on_bt_disconnected (line 170) | def on_bt_disconnected(self):
method on_wifi_connected (line 176) | def on_wifi_connected(self, ssid, ip):
method on_wifi_disconnected (line 184) | def on_wifi_disconnected(self):
method on_plover_boot (line 190) | def on_plover_boot(self):
method on_plover_ready (line 196) | def on_plover_ready(self):
method on_wpm_record (line 204) | def on_wpm_record(self, wpm_top):
method on_dict_lookup_done (line 230) | def on_dict_lookup_done(self):
method hhmmss (line 238) | def hhmmss(self, count, fmt):
Condensed preview — 104 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,088K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 636,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".gitignore",
"chars": 1799,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": "BUILDNOTES.md",
"chars": 4332,
"preview": "# Build Notes\nConnect all the parts before soldering anything in place. They will not function without properly secured "
},
{
"path": "CHANGELOG.md",
"chars": 4028,
"preview": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changel"
},
{
"path": "LICENSE",
"chars": 34523,
"preview": " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C)"
},
{
"path": "README.md",
"chars": 12139,
"preview": "# Stenogotchi\n"
},
{
"path": "plover_plugin/stenogotchi_link/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plover_plugin/stenogotchi_link/clients.py",
"chars": 16110,
"preview": "#!/usr/bin/env python3\n\nimport plover.log\nimport dbus, dbus.exceptions\nfrom dbus.mainloop.glib import DBusGMainLoop\n\nfro"
},
{
"path": "plover_plugin/stenogotchi_link/com.github.stenogotchi.conf",
"chars": 345,
"preview": "<!DOCTYPE busconfig PUBLIC\n \"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN\"\n \"http://www.freedesktop.org/standards"
},
{
"path": "plover_plugin/stenogotchi_link/keymap.py",
"chars": 4177,
"preview": "# Find HID keycode mapping here: https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf\n# Linux keycodes can be"
},
{
"path": "plover_plugin/stenogotchi_link/stenogotchi_link.py",
"chars": 10207,
"preview": "#!/usr/bin/env python3\n# This is a Plover plugin acting as link between Plover and Stenogotchi\n# Based on: https://githu"
},
{
"path": "plover_plugin/stenogotchi_link/wpm.py",
"chars": 8077,
"preview": "#!/usr/bin/env python3\n#\n# Based on: https://github.com/arxanas/plover_wpm_meter.git, modified to remove QT dependency.\n"
},
{
"path": "requirements.txt",
"chars": 245,
"preview": "PyGObject == 3.30.4\ndbus_python == 1.2.16\nevdev == 1.4.0\nRPi.GPIO == 0.7.0\nfile_read_backwards == 2.0.0\ntoml == 0.10.2\ns"
},
{
"path": "stenogotchi/__init__.py",
"chars": 5408,
"preview": "import os\nimport logging\nimport time\nimport re\nimport subprocess\n\nfrom stenogotchi._version import __version__\n\n_name = "
},
{
"path": "stenogotchi/_version.py",
"chars": 21,
"preview": "__version__ = '0.1.0'"
},
{
"path": "stenogotchi/agent.py",
"chars": 4143,
"preview": "import time\nimport json\nimport os\nimport re\nimport logging\nimport asyncio\nimport _thread\n\nimport stenogotchi\nimport sten"
},
{
"path": "stenogotchi/automata.py",
"chars": 2949,
"preview": "import logging\n\nimport stenogotchi.plugins as plugins\n\n\n# basic mood system\nclass Automata(object):\n def __init__(sel"
},
{
"path": "stenogotchi/defaults.toml",
"chars": 8647,
"preview": "# This is a TOML document\n# This is the default config template. To change the default values, please save your modifica"
},
{
"path": "stenogotchi/fs/__init__.py",
"chars": 5820,
"preview": "import os\nimport re\nimport tempfile\nimport contextlib\nimport shutil\nimport _thread\nimport logging\n\nfrom time import slee"
},
{
"path": "stenogotchi/log.py",
"chars": 6563,
"preview": "import hashlib\nimport time\nimport re\nimport os\nimport logging\nimport shutil\nimport gzip\nimport warnings\nfrom datetime im"
},
{
"path": "stenogotchi/plugins/__init__.py",
"chars": 5186,
"preview": "import os\nimport glob\nimport _thread\nimport threading\nimport importlib, importlib.util\nimport logging\n\n\n\ndefault_path = "
},
{
"path": "stenogotchi/plugins/cmd.py",
"chars": 4901,
"preview": "# Handles the commandline stuff\n\nimport os\nimport logging\nimport glob\nimport re\nimport shutil\nfrom fnmatch import fnmatc"
},
{
"path": "stenogotchi/plugins/default/buttonshim.py",
"chars": 20041,
"preview": "# ###############################################################\n# Updated 20-03-2021 by Anodynous\n# - Moved all functi"
},
{
"path": "stenogotchi/plugins/default/dict_lookup.py",
"chars": 10986,
"preview": "#!/usr/bin/env python3\n\nimport logging\nimport time\nimport random\n\nimport stenogotchi.plugins as plugins\nfrom stenogotchi"
},
{
"path": "stenogotchi/plugins/default/evdevkb.py",
"chars": 11250,
"preview": "\"\"\"\nEvdev based keyboard client for capturing input and relays the keypress to a Bluetooth HID keyboard emulator D-BUS S"
},
{
"path": "stenogotchi/plugins/default/example.py",
"chars": 5335,
"preview": "# ###############################################################\n# Based on: https://github.com/evilsocket/pwnagotchi/b"
},
{
"path": "stenogotchi/plugins/default/led.py",
"chars": 5271,
"preview": "# ###############################################################\n# Based on: https://github.com/evilsocket/pwnagotchi/b"
},
{
"path": "stenogotchi/plugins/default/logtail.py",
"chars": 7308,
"preview": "import os\nimport logging\nimport threading\nfrom itertools import islice\nfrom time import sleep\nfrom datetime import datet"
},
{
"path": "stenogotchi/plugins/default/memtemp.py",
"chars": 4157,
"preview": "# memtemp shows memory infos and cpu temperature\n#\n# mem usage, cpu load, cpu temp\n#\n###################################"
},
{
"path": "stenogotchi/plugins/default/plover_link.py",
"chars": 26914,
"preview": "\n\"\"\"\nStenogotchi and Bluetooth HID keyboard emulator D-BUS Service\n\nBased on: https://gist.github.com/ukBaz/a47e71e7b87f"
},
{
"path": "stenogotchi/plugins/default/plover_link_btserver_sdp_record.xml",
"chars": 2585,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\n<record>\n\t<attribute id=\"0x0001\">\n\t\t<sequence>\n\t\t\t<uuid value=\"0x1124\" />\n\t\t</s"
},
{
"path": "stenogotchi/plugins/default/upslite.py",
"chars": 5232,
"preview": "#!/usr/bin/env python3\n\"\"\"\nBased on: https://github.com/linshuqin329/UPS-Lite\nRequires i2c to be enabled in dietpi-confi"
},
{
"path": "stenogotchi/ui/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "stenogotchi/ui/components.py",
"chars": 2519,
"preview": "from PIL import Image\nfrom textwrap import TextWrapper\n\n\nclass Widget(object):\n def __init__(self, xy, color=0):\n "
},
{
"path": "stenogotchi/ui/display.py",
"chars": 2087,
"preview": "import os\nimport logging\nimport threading\n\nimport stenogotchi.plugins as plugins\nimport stenogotchi.ui.hw as hw\nfrom ste"
},
{
"path": "stenogotchi/ui/faces.py",
"chars": 622,
"preview": "LOOK_R = '( ⚆_⚆)'\nLOOK_L = '(☉_☉ )'\nLOOK_R_HAPPY = '( ◕‿◕)'\nLOOK_L_HAPPY = '(◕‿◕ )'\nSLEEP = '(⇀‿‿↼)'\nSLEEP2 = '(≖‿‿≖)'\nA"
},
{
"path": "stenogotchi/ui/fonts.py",
"chars": 1057,
"preview": "from PIL import ImageFont\n\n# should not be changed\nFONT_NAME = 'DejaVuSansMono'\n\n# can be changed\nSTATUS_FONT_NAME = Non"
},
{
"path": "stenogotchi/ui/hw/__init__.py",
"chars": 280,
"preview": "from stenogotchi.ui.hw.waveshare2 import WaveshareV2\n\ndef display_for(config):\n # config has been normalized already "
},
{
"path": "stenogotchi/ui/hw/base.py",
"chars": 1201,
"preview": "import stenogotchi.ui.fonts as fonts\n\n\nclass DisplayImpl(object):\n def __init__(self, config, name):\n self.nam"
},
{
"path": "stenogotchi/ui/hw/libs/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "stenogotchi/ui/hw/libs/waveshare/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "stenogotchi/ui/hw/libs/waveshare/v2/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "stenogotchi/ui/hw/libs/waveshare/v2/epd2in13_V2.py",
"chars": 11080,
"preview": "# //*****************************************************************************\n# * | File :\t epd2in13.py\n# * "
},
{
"path": "stenogotchi/ui/hw/waveshare2.py",
"chars": 3344,
"preview": "import logging\n\nimport stenogotchi.ui.fonts as fonts\nfrom stenogotchi.ui.hw.base import DisplayImpl\n\n\nclass WaveshareV2("
},
{
"path": "stenogotchi/ui/state.py",
"chars": 1603,
"preview": "from threading import Lock\n\n\nclass State(object):\n def __init__(self, state={}):\n self._state = state\n "
},
{
"path": "stenogotchi/ui/view.py",
"chars": 13261,
"preview": "import _thread\nfrom threading import Lock\nimport time\nimport logging\nimport random\nfrom PIL import ImageDraw\n\nimport ste"
},
{
"path": "stenogotchi/ui/web/__init__.py",
"chars": 406,
"preview": "import os\nfrom threading import Lock\n\nframe_path = '/var/tmp/stenogotchi/stenogotchi.png'\nframe_format = 'PNG'\nframe_cty"
},
{
"path": "stenogotchi/ui/web/handler.py",
"chars": 7913,
"preview": "import logging\nimport os\nimport _thread\nimport secrets\nimport subprocess\nfrom functools import wraps\n\n# https://stackove"
},
{
"path": "stenogotchi/ui/web/server.py",
"chars": 1696,
"preview": "import _thread\nimport secrets\nimport logging\nimport os\n\n# https://stackoverflow.com/questions/14888799/disable-console-m"
},
{
"path": "stenogotchi/ui/web/static/css/jquery.jqplot.css",
"chars": 5624,
"preview": "/*rules for the plot target div. These will be cascaded down to all plot elements according to css rules*/\n.jqplot-targ"
},
{
"path": "stenogotchi/ui/web/static/css/style.css",
"chars": 1847,
"preview": ".ui-image {\n width: 100%;\n}\n\n.pixelated {\n image-rendering: optimizeSpeed; /* Legal fallback */\n image-renderin"
},
{
"path": "stenogotchi/ui/web/static/js/jquery.jqplot.js",
"chars": 469424,
"preview": "/**\n * Title: jqPlot Charts\n * \n * Pure JavaScript plotting plugin for jQuery.\n * \n * About: Version\n * \n * version: 1.0"
},
{
"path": "stenogotchi/ui/web/static/js/jquery.mobile/jquery.mobile-1.4.5.css",
"chars": 239560,
"preview": "/*!\n* jQuery Mobile 1.4.5\n* Git HEAD hash: 68e55e78b292634d3991c795f06f5e37a512decc <> Date: Fri Oct 31 2014 17:33:30 UT"
},
{
"path": "stenogotchi/ui/web/static/js/jquery.mobile/jquery.mobile-1.4.5.js",
"chars": 465714,
"preview": "/*!\n* jQuery Mobile 1.4.5\n* Git HEAD hash: 68e55e78b292634d3991c795f06f5e37a512decc <> Date: Fri Oct 31 2014 17:33:30 UT"
},
{
"path": "stenogotchi/ui/web/static/js/jquery.mobile/jquery.mobile.external-png-1.4.5.css",
"chars": 122128,
"preview": "/*!\n* jQuery Mobile 1.4.5\n* Git HEAD hash: 68e55e78b292634d3991c795f06f5e37a512decc <> Date: Fri Oct 31 2014 17:33:30 UT"
},
{
"path": "stenogotchi/ui/web/static/js/jquery.mobile/jquery.mobile.icons-1.4.5.css",
"chars": 129103,
"preview": "/*!\n* jQuery Mobile 1.4.5\n* Git HEAD hash: 68e55e78b292634d3991c795f06f5e37a512decc <> Date: Fri Oct 31 2014 17:33:30 UT"
},
{
"path": "stenogotchi/ui/web/static/js/jquery.mobile/jquery.mobile.inline-png-1.4.5.css",
"chars": 149156,
"preview": "/*!\n* jQuery Mobile 1.4.5\n* Git HEAD hash: 68e55e78b292634d3991c795f06f5e37a512decc <> Date: Fri Oct 31 2014 17:33:30 UT"
},
{
"path": "stenogotchi/ui/web/static/js/jquery.mobile/jquery.mobile.inline-svg-1.4.5.css",
"chars": 226959,
"preview": "/*!\n* jQuery Mobile 1.4.5\n* Git HEAD hash: 68e55e78b292634d3991c795f06f5e37a512decc <> Date: Fri Oct 31 2014 17:33:30 UT"
},
{
"path": "stenogotchi/ui/web/static/js/jquery.mobile/jquery.mobile.structure-1.4.5.css",
"chars": 91343,
"preview": "/*!\n* jQuery Mobile 1.4.5\n* Git HEAD hash: 68e55e78b292634d3991c795f06f5e37a512decc <> Date: Fri Oct 31 2014 17:33:30 UT"
},
{
"path": "stenogotchi/ui/web/static/js/jquery.mobile/jquery.mobile.theme-1.4.5.css",
"chars": 19892,
"preview": "/*!\n* jQuery Mobile 1.4.5\n* Git HEAD hash: 68e55e78b292634d3991c795f06f5e37a512decc <> Date: Fri Oct 31 2014 17:33:30 UT"
},
{
"path": "stenogotchi/ui/web/static/js/jquery.timeago.js",
"chars": 7404,
"preview": "/**\n * Timeago is a jQuery plugin that makes it easy to support automatically\n * updating fuzzy timestamps (e.g. \"4 minu"
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.BezierCurveRenderer.js",
"chars": 14541,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.barRenderer.js",
"chars": 34872,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.blockRenderer.js",
"chars": 9158,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.bubbleRenderer.js",
"chars": 31083,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.canvasAxisLabelRenderer.js",
"chars": 8146,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.canvasAxisTickRenderer.js",
"chars": 9838,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.canvasOverlay.js",
"chars": 44645,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.canvasTextRenderer.js",
"chars": 24367,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.categoryAxisRenderer.js",
"chars": 28566,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.ciParser.js",
"chars": 4141,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.cursor.js",
"chars": 46097,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.dateAxisRenderer.js",
"chars": 30345,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.donutRenderer.js",
"chars": 34621,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.dragable.js",
"chars": 9490,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.enhancedLegendRenderer.js",
"chars": 13740,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.enhancedPieLegendRenderer.js",
"chars": 11294,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.funnelRenderer.js",
"chars": 39823,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.highlighter.js",
"chars": 21364,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.json2.js",
"chars": 17280,
"preview": "/*\n 2010-11-01 Chris Leonello\n \n Slightly modified version of the original json2.js to put JSON\n functions u"
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.logAxisRenderer.js",
"chars": 21777,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.mekkoAxisRenderer.js",
"chars": 25534,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.mekkoRenderer.js",
"chars": 18948,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.meterGaugeRenderer.js",
"chars": 42897,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.mobile.js",
"chars": 2050,
"preview": "/**\n * jqplot.jquerymobile plugin\n * jQuery Mobile virtual event support.\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.ohlcRenderer.js",
"chars": 15251,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.pieRenderer.js",
"chars": 37544,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.pointLabels.js",
"chars": 14593,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.pyramidAxisRenderer.js",
"chars": 29621,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.pyramidGridRenderer.js",
"chars": 22493,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.pyramidRenderer.js",
"chars": 21129,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/plugins/jqplot.trendline.js",
"chars": 7589,
"preview": "/**\n * jqPlot\n * Pure JavaScript plotting plugin using jQuery\n *\n * Version: 1.0.9\n * Revision: d96a669\n *\n * Copyright "
},
{
"path": "stenogotchi/ui/web/static/js/viewportHeight.js",
"chars": 587,
"preview": "/* https://css-tricks.com/the-trick-to-viewport-units-on-mobile/*/\n\nvar lastViewportHeight;\n\nfunction updateViewportSize"
},
{
"path": "stenogotchi/ui/web/templates/base.html",
"chars": 2401,
"preview": "<!DOCTYPE html>\n<html>\n{% block head %}\n<head>\n {% block meta %}\n <meta charset=\"utf-8\">\n <meta name=\"viewport\""
},
{
"path": "stenogotchi/ui/web/templates/index.html",
"chars": 1845,
"preview": "{% extends \"base.html\" %}\n{% set active_page = \"home\" %}\n\n{% block title %}\n{{ title }}\n{% endblock %}\n\n{% block script "
},
{
"path": "stenogotchi/ui/web/templates/plugins.html",
"chars": 2330,
"preview": "{% extends \"base.html\" %}\n{% set active_page = \"plugins\" %}\n\n{% block title %}\nPlugins\n{% endblock %}\n\n{% block styles %"
},
{
"path": "stenogotchi/ui/web/templates/status.html",
"chars": 282,
"preview": "<html>\n <head>\n <title>{{ title }}</title>\n <meta http-equiv=\"refresh\" content=\"{{ go_back_after }};URL=/\">\n "
},
{
"path": "stenogotchi/utils.py",
"chars": 10427,
"preview": "import logging\nimport glob\nimport os\nimport sys\nimport time\n\nimport json\nimport shutil\nimport toml\nimport sys\nimport re\n"
},
{
"path": "stenogotchi/voice.py",
"chars": 8999,
"preview": "import random\nimport gettext\nimport os\n\n\nclass Voice:\n def __init__(self, lang):\n localedir = os.path.join(os."
},
{
"path": "stenogotchi.py",
"chars": 4051,
"preview": "#!/usr/bin/python3\nimport logging\nimport argparse\nimport time\nimport signal\nimport sys\nimport toml\n\nimport stenogotchi\nf"
}
]
About this extraction
This page contains the full source code of the Anodynous/stenogotchi GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 104 files (2.8 MB), approximately 742.4k tokens, and a symbol index with 862 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.