Compare commits

...

476 Commits

Author SHA1 Message Date
Nicholas Hope 16eb0db3ee created typo in app title 2022-04-10 11:54:16 -04:00
Marek Ventur 21696bf58a
Merge pull request #158 from mbooth101/settlers_game
Settlers of EMF
2018-11-25 11:25:28 +00:00
Mat Booth a6747e79f2 Settlers of EMF
Add a noddy game over screen that triggers when you end your turn
with 10 points or more. Menu object was enhanced to allow multi-
line questions to accomodate this.
2018-11-24 14:42:07 +00:00
Mat Booth 82c0cb378c Settlers of EMF
UI improvements: Always use menu button to summon a menu screen,
add a status line that gives hints about controls etc, allow
using the B button as a shortcut for "Back" in menus that have
such an option

Also add a kind of "cheat mode" for testing that grants extra
resources
2018-11-24 13:49:11 +00:00
Mat Booth df0a13c1ab Settlers of EMF
Finish implementing town building selection mode -- all building
modes now complete
2018-11-07 23:47:29 +00:00
Mat Booth 87457fccb4 Settlers of EMF
Fix a problem with the road selection cursor getting stuck due to
there being duplicate entries in the candidate list
2018-11-05 23:49:00 +00:00
Mat Booth 1fde67e2f8 Settlers of EMF
Refactor roads and settlements to abstract out some common features
and finish adding implementations of building roads and towns. Also
implement drawing a selection box around hex edges when choosing
where to build roads.
2018-11-05 03:37:23 +00:00
Mat Booth 31d326f281 Settlers of EMF
Turn robber mode into a more general interactive selection mode,
add a way to show which settlement is selected and and implement
city building
2018-11-04 16:38:15 +00:00
Mat Booth bdbaa2f62e Settlers of EMF
Refactor menu drawing to allow menus to customise how choices are
drawn. Make the trade menu more sensible to use, showing better
the trade costs. Implement simple 4 for 1 trading with the bank
2018-11-03 18:18:53 +00:00
Mat Booth 25f35c0518 Settlers of EMF
Ensure 'back' option is visible on trade menu.
2018-11-03 15:21:25 +00:00
Mat Booth fcd537cdfd Settlers of EMF
Allow menu screens to avoid redrawing the logo, for quicker
response times when navigating menus.
2018-10-30 19:56:18 +00:00
Mat Booth 846bab0ab1 Settlers of EMF
Implement pass-and-play multiplayer, with interstitial screen asking
to pass the badge to the next player
2018-10-30 18:51:29 +00:00
Mat Booth 01e0b9226d Settlers of EMF
Keep track of and show turn numbers, plus prevent starting
settlements from being created too close to one another
2018-10-30 18:51:29 +00:00
Mat Booth c75357269f Settlers of EMF
Show cost of building stuff in the build menu and only enable
options that the player can afford to build
2018-10-30 18:51:29 +00:00
Mat Booth b8a49997e7 Settlers of EMF
Allow moving the robber around when a 7 is rolled
2018-10-30 18:51:29 +00:00
Mat Booth dac613c19f Settlers of EMF
Refactor resources and collect resources on dice roll
2018-10-30 18:51:29 +00:00
Mat Booth 57ee3129ac Settlers of EMF
Beginnings of an EMF themed implementation of Settlers of Catan
2018-10-30 18:51:29 +00:00
Dave Arter 4cbfd23e05
Merge pull request #156 from gavinatkinson/master
Set bootstrapped = False on two apps that don't need to be.
2018-09-28 17:48:39 +01:00
Gavin Atkinson 0d5ef24391 Set ___bootstrapped___ = False on two apps that don't need to be.
- home_pycon: Home screen for PyCon, not relevent to most EMF users
 - orbs: Game no longer usable after EMF as server is offline

While here, correct comment in home_pycon explaining what it is.
2018-09-28 17:29:42 +01:00
Marek Ventur b7ae5a7429
Merge pull request #155 from janion/master
Added neopixel colour picker
2018-09-23 11:54:33 +01:00
janion 824ea75785
Added a colour picker for the neopixels
Shows a colour palette from which to select a colour
2018-09-21 17:07:37 +01:00
janion 00151d2d9d
Merge pull request #2 from emfcamp/master
merge from master
2018-09-21 17:01:39 +01:00
Marek Ventur 055746c171
Merge pull request #152 from janion/master
Updated Sketchy-Etch
2018-09-20 22:25:05 +01:00
Marek Ventur 650e24bcdc
Merge pull request #153 from CrazyRobMiles/air_quality
Air Quality Sensor Badge
2018-09-20 22:24:20 +01:00
Marek Ventur d4343e67a8
Merge pull request #154 from tswsl1989/master
Fix bootstrapped apps filter when no path specified
2018-09-20 22:23:06 +01:00
Thomas Lake a130e161dd Fix bootstrapped apps filter when no path specified 2018-09-19 20:42:52 +01:00
Rob Miles e3f978e1d3 Air Quality Sensor Badge 2018-09-19 15:32:56 +01:00
janion 7c6b53d691
Updated Sketchy-Etch
Added colour picking, circle drawing, and clearing
2018-09-17 20:53:49 +01:00
janion ddd5bb9f09
Merge pull request #1 from emfcamp/master
Update from head
2018-09-17 20:49:08 +01:00
Alistair MacDonald 20b1d31a1b
Merge pull request #148 from emfcamp/sammachin-gprs
Sammachin gprs
2018-09-16 18:09:21 +01:00
Marek Ventur ce621a704b
use native dependency for random 2018-09-16 17:48:21 +01:00
Marek Ventur 47958ff566
Merge pull request #132 from mstratford/mstratford-dialogs
Badge Store UI/UX improvements & dialog alignments
2018-09-16 16:29:35 +01:00
Marek Ventur bb70d3f359
Merge branch 'master' into mstratford-dialogs 2018-09-16 16:29:10 +01:00
Marek Ventur 49036697e6
don't bootstrap this app 2018-09-16 16:27:36 +01:00
Marek Ventur bef3fea0fe
Merge pull request #109 from AnthonyMann/master
Initial Aerospace badge
2018-09-16 16:27:11 +01:00
Marek Ventur 7ab2c9e18a
clarify "brainfuck" 2018-09-16 16:26:27 +01:00
Marek Ventur b635e388ae
Merge pull request #112 from mitxela/master
simple bf interpreter
2018-09-16 16:25:33 +01:00
Marek Ventur f2230aa52b
Merge pull request #116 from crc48/master
Cards against EMF
2018-09-16 16:23:12 +01:00
Marek Ventur fd5a03a09d
Merge pull request #119 from Trikkitt/orbs
Orbs
2018-09-16 16:21:20 +01:00
Marek Ventur 9a30c6667c
Merge pull request #127 from catalin-ursachi/mass-storage-confirm
Add warning prompt to mass_storage
2018-09-16 16:20:45 +01:00
Marek Ventur 2d335fd6f6
Merge pull request #136 from Illlin/master
Add DevRant client to TiLDA-Mk4
2018-09-16 16:15:22 +01:00
Marek Ventur 51462746e6
Merge pull request #137 from mallyhubz/nyan_home
nyan cat homescreen
2018-09-16 16:14:04 +01:00
Marek Ventur df9b1cf9e3
Merge pull request #140 from tswsl1989/setlauncher
Allow default launcher to be changed
2018-09-16 16:13:21 +01:00
Marek Ventur 7716ee6056
Merge pull request #151 from FLamparski/docs
Fix database.py docstring
2018-09-16 16:12:45 +01:00
Marek Ventur 64d719f08d
Merge pull request #146 from davea/use-update-endpoint
Call /update instead of /install on the backend when updating apps
2018-09-16 16:11:45 +01:00
Filip Wieland f14fb67fd9 Fix database.py docstring 2018-09-16 12:16:15 +01:00
Marek Ventur 3c228d161b
Merge pull request #147 from oscitancy/master
+ Added function to exit the Holland app
2018-09-15 23:23:52 +01:00
Marek Ventur a0038d98ac
Merge pull request #149 from Molive-0/master
Add Arp music synth
2018-09-15 23:23:34 +01:00
Marek Ventur de4fe0fb64
Merge pull request #150 from FLamparski/trains
Add Trains app
2018-09-15 23:22:38 +01:00
Filip Wieland ff7c10636e Show wifi wait message 2018-09-15 23:01:29 +01:00
Filip Wieland 825741bdd5 Bottom label, quit with MENU 2018-09-15 22:28:38 +01:00
Filip Wieland e86d5955a3 Settings screen, spit and polish 2018-09-15 22:20:20 +01:00
Matthew Stratford 484f7c4063 Fix right-alignment (clipping) on WaitingMessage 2018-09-15 16:08:32 +01:00
Matthew Stratford 7316522b0e Give updating dialog a nice title 2018-09-15 16:01:31 +01:00
Matthew Stratford d5c64f4de7 Back from Install page to category 2018-09-15 15:59:37 +01:00
Matthew Stratford 268ef625f2 Allow scrolling of labels by \n 2018-09-15 15:52:59 +01:00
Kimball Johnson 4a6271de07 Add homescreen for PyCon 2018-09-15 09:53:09 +01:00
Matthew Stratford 705167983d
Merge branch 'master' into mstratford-dialogs 2018-09-14 23:34:22 +01:00
Matthew Stratford 6683e2bc8d Tidy up with .format() 2018-09-14 23:26:48 +01:00
Sam Machin 912cc34269
Create main.py
Sample application for using Hologram with GPRS
2018-09-14 08:02:55 +01:00
Sam Machin 098a07e0a4
Update sim800.py
Few more tweaks to GPRS handling after real world testing with an app
2018-09-14 08:00:41 +01:00
Filip Wieland f160547e7d Departure board for Deptford 2018-09-12 22:26:48 +01:00
Molive fade33fd3d
Add Arp music synth 2018-09-12 22:17:02 +01:00
Thomas Lake edb57cbe3b Allow default launcher selection
This allows e.g. speedlauncher to be used with any of the homescreen apps
2018-09-12 21:06:20 +01:00
Sam Machin a19445f862
Update sim800.py
Added GPRS support, `setup_gprs()` will prepare the sim800 for data, `connect_gprs("hologram")` will connect to the hologram APN
Also added a function to start a tcp server on the SIM 800 using `start_server(4010)` to listen on port 4010, the payload will be a binary object passed to whatever is defined as `server_callback`
2018-09-11 19:04:26 +01:00
Emily 1a4b656270 + Added function to exit app 2018-09-11 18:22:32 +01:00
Sam Machin 76463fe172
Update sim800.py
Added functions to setup and connect GPRS data, starting a TCP server and callback handler for server data
2018-09-11 08:15:27 +01:00
Dave Arter 1ee0067f23 Call /update instead of /install on the backend when updating apps
The badge has no way of knowing whether an installed app came from the
badge store or local development, so simply requests all installed apps
when doing an update. The default /install path on the backend will
return a 404 if any of the requested apps aren't in the repo.

This commit calls /update instead, which doesn't 404 if an app is
missing.

Depends on emfcamp/Mk4-Backend#2

Fixes #57
2018-09-10 14:15:41 +01:00
Dave Arter 49125fc3d2 Fix badge store app installation when custom apps are present
Fixes #85.
See also #57, though doesn’t fix ‘update all apps’ functionality.
2018-09-10 13:36:12 +01:00
Bob Clough 6fb00ff971
Merge pull request #141 from tswsl1989/limitsync
Optionally limit sync to bootstrapped apps
2018-09-10 10:08:47 +01:00
Dave Arter bdae1dba3b Allow ‘z’ key to be typed on keypad
oops.
2018-09-09 21:01:24 +01:00
Thomas Lake dbb8515fb8 Don't remove paths explicitly specified on command line 2018-09-09 18:45:11 +01:00
Thomas Lake d72cdc9272 Optionally limit sync to bootstrapped apps
tilda_tools sync # Syncs everything, as before
tilda_tools --bootstrapped-apps sync # Removes any apps not marked as bootstrapped in their main.py

Fixes #123
2018-09-09 18:39:43 +01:00
Dave Arter 68a08fc47d
Merge pull request #139 from robberwick/horse_melon
Add Horse Praise and Melon Worship app
2018-09-09 17:29:23 +01:00
Rob Berwick c6342ff214 Add Horse Praise and Melon Worship app 2018-09-09 16:21:57 +01:00
Filip Wieland bc0fa1f8cb Trains - Initial commit 2018-09-09 13:14:30 +01:00
Dave Arter f5e61c6b9a
Merge pull request #108 from mellmann/nyan
nyan cat animation
2018-09-09 10:28:04 +01:00
Heinrich Mellmann 49d0356991 removed unnecessary images
show every frame
2018-09-09 11:14:18 +02:00
Dave Arter ce478e2aa2 Show badge firmware version in sysinfo app 2018-09-08 11:05:09 +01:00
Dave Arter 7835018432 Remove lib/random dependency from pong 2018-09-07 21:29:14 +01:00
Dave Arter d696b27ba1
Merge pull request #129 from catalin-ursachi/fix-tildr
TILDR-1073 - Fix profile initialization
2018-09-07 21:15:27 +01:00
Dave Arter 593ee2a469
Merge pull request #120 from MisguidedEmails/pride
Pride add selection of flags
2018-09-07 21:10:05 +01:00
Dave Arter c4f6bc8549
Merge pull request #138 from mbooth101/settlers
Initial version of a Catan game board generator
2018-09-07 21:05:18 +01:00
Dave Arter 9b0c1006ef
Merge pull request #117 from cibomahto/pong
Pong!
2018-09-07 21:00:33 +01:00
Dave Arter a391ab48f8
Merge pull request #115 from victorloux/master
App: speed launcher
2018-09-07 20:47:40 +01:00
Dave Arter d185a9c92c Remove EMF category 2018-09-07 20:38:08 +01:00
Dave Arter 28afed2c25 Merge remote-tracking branch 'upstream/pull/113' 2018-09-07 20:36:09 +01:00
Dave Arter af2970c2e3
Merge pull request #111 from cHemingway/master
Add app to use badge as bluetooth speaker
2018-09-07 19:32:25 +01:00
Dave Arter 46f71f57f6
Merge pull request #107 from ganbariley/master
Adds torch capability to emfcampqueer_home app
2018-09-07 19:08:31 +01:00
Dave Arter 918c6bbeb9
Merge pull request #133 from janion/master
Updated Sketchy-Etch
2018-09-07 18:49:34 +01:00
Dave Arter e52f0baa56
Merge pull request #124 from jake-walker/add-titles
Add ___title___ to all apps
2018-09-07 17:59:17 +01:00
Mat Booth 7ac28fc73e Initial version of a Catan game board generator
If you have 3D printed your own version of Settlers of Catan, with
3D terrain and little sheep and grain siloes and everything, it is
fairly difficult to properly randomise the tile selection during
the game setup.

I assume most EMF-goers have encountered this problem, so this app
provides a solution!
2018-09-07 14:46:31 +01:00
Bob Clough b44cc3596b
Merge pull request #128 from pacohope/use-system-random
Use underlying system random instead of homebrew random
2018-09-06 18:06:18 +01:00
Bob Clough 5303164d8f
Merge pull request #122 from SteveAmor/patch-2
Add pybcdc.inf to root for windows users
2018-09-06 10:31:59 +01:00
Bob Clough b74a84f5f5
Merge pull request #125 from pacohope/numbered-menu
Make menu items numbered
2018-09-06 10:27:51 +01:00
Bob Clough 91cbf440ea
Merge pull request #134 from philcrump/patch-2
lib/ntp: Remove onsite NTP server. NTP pool is now primary.
2018-09-06 10:20:55 +01:00
mallyhubz e9415827b7 fixed menu long press 2018-09-06 01:00:33 +01:00
mallyhubz 1531455542 fixed dependency metadata 2018-09-06 00:34:43 +01:00
mallyhubz 29cf7896dc nyan cat homescreen 2018-09-06 00:22:41 +01:00
Illlin e941220c41
Done more dependency changeing 2018-09-05 19:49:39 +01:00
Illlin 24c8935cd0
Fixed dependency error 2018-09-05 19:41:57 +01:00
Illlin 6e4eb32905
Added DevRant for the TiLDA-Mk4
DevRant text only client for the TiLDA-Mk4. Only supports viewing posts
2018-09-05 19:32:27 +01:00
Phil Crump a1bbd70282
lib/ntp: Remove onsite NTP server. NTP pool is now primary. 2018-09-05 18:11:53 +01:00
Paco Hope 1dc2f765b0 Merged in prior branch of fixing random. 2018-09-05 15:41:20 +01:00
Paco Hope 2e9aa2e345 Removed dependency on random 2018-09-05 15:38:33 +01:00
Paco Hope 9de6896388 removed random dependency, added app import 2018-09-05 15:38:11 +01:00
Paco Hope efab551165 deleted unneeded 2018-09-05 15:37:37 +01:00
janion 10aaf19204
Updated Sketchy-Etch
Added option to clear drawing, added initial dialog with instructions, added ability to create circles
2018-09-05 13:22:29 +01:00
Matthew Stratford 900c711ede Fix alignments of dialogs. 2018-09-05 00:39:54 +01:00
Matthew Stratford b33e075f5d Improve Remove App messages. 2018-09-05 00:38:49 +01:00
Matthew Stratford 93897d2405 Better remove app titles 2018-09-04 21:27:30 +01:00
Matthew Stratford 15f63daf48 Nicer update dialogs. 2018-09-04 20:10:02 +01:00
Matthew Stratford e46aed13c2 Category back to all cats. 2018-09-04 19:30:47 +01:00
Matthew Stratford 5c3188ac8b Improve install menu dialogues. 2018-09-04 19:25:50 +01:00
Matthew Stratford 85cbc6cb84 Post install launch and back 2018-09-04 18:28:12 +01:00
Matthew Stratford 1891ef659f Improve installing status
Also makes long app names not overflow title.
2018-09-04 17:08:01 +01:00
Catalin Ursachi ab1e6ad2bd TILDR-1073 - Fix profile initialization 2018-09-04 02:06:57 +01:00
Catalin Ursachi f969b16afc Add warning prompt to mass_storage 2018-09-03 23:43:00 +01:00
Paco Hope 6bad9c700f remove unreferenced _bigrand() function 2018-09-03 23:40:01 +01:00
Paco Hope 736da656bc Use system random instead of homebrew random 2018-09-03 23:16:04 +01:00
rdmonk 1d02cdbc8d
Merge pull request #126 from catalin-ursachi/patch-1
Make Holland a non-bootstrapped app
2018-09-03 22:42:01 +01:00
Catalin Ursachi 88fb6e3ba8
Make Holland a non-bootstrapped app 2018-09-03 22:36:46 +01:00
Paco Hope db33131b1d make menu items numbered 2018-09-03 21:34:32 +01:00
Jake Walker fc65172eaf Add ___title___ to all apps 2018-09-03 14:59:37 +01:00
Steveis dd019601c5
Add pybcdc.inf to root as per readme.txt 2018-09-03 08:35:26 +01:00
Heinrich Mellmann 4fb521ece9 start with the correct rotation 2018-09-03 02:48:27 +01:00
MisguidedEmails 229f09acd4 Only render the flag where it's visible.
We now don't have to re-render the name on each flag change.
2018-09-02 23:42:55 +01:00
MisguidedEmails f19cffd307 Added a selection of flags the user can switch between. 2018-09-02 23:36:19 +01:00
mjt@mjturner.co.uk 4ff0aa12f5 Orbs Game 2018-09-02 23:26:02 +01:00
mjt@mjturner.co.uk f2f352bed7 Orbs Game 2018-09-02 23:17:14 +01:00
mjt@mjturner.co.uk 9b916093e4 Orbs Game 2018-09-02 23:06:18 +01:00
Matt Mets 6fa8e86097 Pong! 2018-09-02 22:27:35 +01:00
Heinrich Mellmann e20e2c8295 removed FUN 2018-09-02 22:23:26 +01:00
Victor 5f720bb2df Unique name 2018-09-02 22:17:28 +01:00
Victor ceb3f06b7c Speedlauncher app 2018-09-02 22:15:05 +01:00
Calantha 4d046a9c6c First Commit 2018-09-02 22:13:41 +01:00
MisguidedEmails 599c504110 Fixed name orientation 2018-09-02 22:04:10 +01:00
Heinrich Mellmann 9e7768f9f5 rotate the acreen with the A button 2018-09-02 22:01:52 +01:00
MisguidedEmails 1cad355303 Removed unecessary variable assignment. 2018-09-02 21:53:42 +01:00
MisguidedEmails 17d40f9c62 Sectioned code into functions 2018-09-02 21:53:11 +01:00
Heinrich Mellmann 0e4664e99f correct category 2018-09-02 21:35:49 +01:00
Heinrich Mellmann df307c85f1 serendipity! 2018-09-02 21:35:18 +01:00
Chris Hemingway 61935998ba Add app to use badge as bluetooth speaker 2018-09-02 21:28:54 +01:00
Tim Jacobs 0bbf655cc2 simple bf interpreter 2018-09-02 21:28:04 +01:00
Heinrich Mellmann bd9214ca4f better categories
corrected the name
2018-09-02 20:44:32 +01:00
Heinrich Mellmann ea236d4c7b local images 2018-09-02 20:42:58 +01:00
Heinrich Mellmann 85365a8a1d Merge remote-tracking branch 'origin/master' 2018-09-02 20:40:49 +01:00
Anthony Mann 2f93b6ee0b Initial Aerospace badge
initial commit!
2018-09-02 20:20:27 +01:00
Heinrich Mellmann d5998a9f06 nyan cat animation 2018-09-02 20:20:03 +01:00
MisguidedEmails f1ff8bef60 Changed center to center_width 2018-09-02 20:20:01 +01:00
MisguidedEmails 5a0a0f9d4b Removed "Hi I'm" 2018-09-02 20:19:06 +01:00
ganbariley 4947abb93b
Merge pull request #2 from skylarmacdonald/patch-1
Add torch magic
2018-09-02 19:43:52 +01:00
Skylar MacDonald 95008d4323
Add torch magic 2018-09-02 19:43:06 +01:00
Marek Ventur 42e2a5e9c2
Merge pull request #105 from Marneus68/master
Fixed avatar homescreen app category
2018-09-02 19:28:48 +01:00
Marek Ventur a48bfb3d59
Merge pull request #104 from cassm/master
Fix two bugs in sequencer
2018-09-02 19:28:05 +01:00
Marek Ventur 6168dee15a
Merge pull request #103 from renzenicolai/master
Bugfixes and strobe
2018-09-02 19:27:49 +01:00
Duane Bekaert c3703ab07c Fixed avatar homescreen app category 2018-09-02 19:20:57 +02:00
Cass May e6df1e9053 Merge upstream work 2018-09-02 17:57:12 +01:00
Cass May a0727ef962 Fix several sequencer bugs 2018-09-02 17:53:07 +01:00
Renze Nicolai 2f615673fc Brenno! 2018-09-02 18:47:43 +02:00
Dave Arter 8bf11d271f Default to numeric input for phone number inputs 2018-09-02 17:43:00 +01:00
Marek Ventur 966958873f
Fix master 2018-09-02 17:40:39 +01:00
Marek Ventur a54f380cbd
Merge pull request #91 from Skybound1/master
Adds party app
2018-09-02 17:39:47 +01:00
Marek Ventur 0c1ee01e68
Merge pull request #92 from cassm/master
Add sequencer app
2018-09-02 17:39:23 +01:00
Marek Ventur ace7f5b87e
Merge pull request #93 from tswsl1989/master
BTScan - Bluetooth device scan app
2018-09-02 17:38:47 +01:00
Marek Ventur f1eb3a6828
Merge pull request #90 from skylarmacdonald/home_trans
Added trans home screen app
2018-09-02 17:38:28 +01:00
Marek Ventur 9ee6e5a631
Merge pull request #89 from janion/master
Accidentally created etcher sketch
2018-09-02 17:37:17 +01:00
Marek Ventur dac17bd88c
Merge pull request #88 from shokinn/master
Fix sleep_or_exit timer. Set to default (0.5)
2018-09-02 17:37:00 +01:00
Marek Ventur ad3efb0c27
Merge pull request #94 from Pezmc/master
Disco Screen
2018-09-02 17:36:16 +01:00
Marek Ventur 6b393368d8
Merge pull request #96 from ganbariley/master
Adds app EMFCamp Pride Homepage
2018-09-02 17:35:48 +01:00
Marek Ventur ca0462f61a
Merge pull request #97 from Floppy/3dspin
add ported 3d spinning object app
2018-09-02 17:35:25 +01:00
Renze Nicolai 069b6a33fc Bugfixes and strobe 2018-09-02 18:34:59 +02:00
Marek Ventur 0d87808f14
Merge pull request #98 from Pezmc/star_wars
🎉 Star wars logo and plays music
2018-09-02 17:34:45 +01:00
Marek Ventur 65ee5a6dd9
Merge pull request #99 from renzenicolai/master
Add the Holland app
2018-09-02 17:34:27 +01:00
Marek Ventur ed21609570
Merge pull request #100 from tomsci/tomsci_pageupdn
Added page up and page down to prompt_option dialogs
2018-09-02 17:33:13 +01:00
Marek Ventur b2e571cfeb
fix buils 2018-09-02 17:32:29 +01:00
Marek Ventur c168726c29
Merge pull request #101 from eastabrooka/master
App : Plays the Mario theme, Displays picture of Mario.
2018-09-02 17:32:01 +01:00
Marek Ventur b742f1dfb9
Merge pull request #102 from betafrogg/master
add enby flag homescreen
2018-09-02 17:31:03 +01:00
James Smith ec9580719b
fix indentation
thanks atom
2018-09-02 17:23:28 +01:00
Tom Sutcliffe d2cace4ad1 Added page up and page down to prompt_option dialogs
Using * and #. Also made 0-9 jump to that item.
2018-09-02 17:16:59 +01:00
James Smith f2db60fd22
added proper exiting 2018-09-02 17:16:12 +01:00
Renze Nicolai 700ac52b99 Splashscreen 2018-09-02 18:13:15 +02:00
Renze Nicolai 1948c74759 Moar leds! 2018-09-02 18:00:40 +02:00
frogg 168fe8caca Remove Hi I'm 2018-09-02 16:49:33 +01:00
frogg ca4e2c84fd update text 2018-09-02 16:44:03 +01:00
Renze Nicolai e25928e060 Small fixes 2018-09-02 17:42:47 +02:00
frogg 87085cb1f6 Update color 2018-09-02 16:36:37 +01:00
Renze Nicolai 61dae74198 Holland app 2018-09-02 17:33:21 +02:00
ganbariley 0fccae65f6
Merge pull request #1 from skylarmacdonald/patch-1
Remove dependency that's included in bundle
2018-09-02 16:23:28 +01:00
Skylar MacDonald ecbdbe0469
Remove dependency that's included in bundle
this will fix the failing validation
2018-09-02 16:22:22 +01:00
Alex Eastabrook 2450db075b Adding the local changes of my Mario Theme to my Branch. 2018-09-02 16:20:04 +01:00
Pez Cuckow 7226c3e98f 🎉 Star wars logo and plays music 2018-09-02 16:17:42 +01:00
James Smith c58c9f7cc0
add ported 3d spinning object app 2018-09-02 16:14:24 +01:00
ganbariley b5b6f33b61 Fixed main.py so it does not bootstrap 2018-09-02 15:56:40 +01:00
ganbariley 176d156383 EMFCamp Pride Homepage!
Gives the EMFCamp logo a rainbow makeover!
2018-09-02 15:46:14 +01:00
Skylar MacDonald 5cacbe0e83
Fixed incorrect image paths 2018-09-02 15:26:37 +01:00
Pez Cuckow b68cd168d8 🔧 Set category 2018-09-02 15:26:20 +01:00
Pez Cuckow d968630cdc 💄 Fake more random patterns 2018-09-02 15:25:08 +01:00
Thomas Lake 9f78ea99c8 Improve scan UI, slightly 2018-09-02 15:13:50 +01:00
Cass May 69d5fce84a Add sequencer app 2018-09-02 15:11:44 +01:00
Thomas Lake aee5d6b2b6 Append to BTScan results, don't replace 2018-09-02 15:11:03 +01:00
frogg e4ba01098c message 2018-09-02 15:01:31 +01:00
Mohit Gupta d5129443f0 Adds party app 2018-09-02 14:59:34 +01:00
Skylar MacDonald 09c50dc47f Added trans home screen app 2018-09-02 14:59:09 +01:00
Thomas Lake b12b077b16 Basic Bluetooth scan app 2018-09-02 14:58:57 +01:00
janion 271fe31f14
Accidentally created etcher sketch 2018-09-02 14:55:35 +01:00
Philip Henning db78a934a8 Fix sleep_or_exit timer. Set to default (0.5) 2018-09-02 15:44:30 +02:00
Marek Ventur 627ab26fc3
Merge pull request #74 from davegoopot/master
Fixing #73
2018-09-02 14:22:03 +01:00
Marek Ventur 8a8e43ac48
Merge pull request #75 from Davermouse/default-app-fix
Fix selection of default app
2018-09-02 14:21:07 +01:00
Marek Ventur b8afa052b9
Merge pull request #77 from bboortz/hello2
Close added to hello_world app
2018-09-02 14:20:35 +01:00
Marek Ventur 14a99c1ca7
Merge pull request #78 from shokinn/master
Stratum 0 flavored homescreen
2018-09-02 14:20:21 +01:00
Marek Ventur fbe56b9449
Merge pull request #79 from Marneus68/master
Added Avatar, a new homescreen grabbing an image from an URL
2018-09-02 14:19:55 +01:00
Marek Ventur 6326007b6d
Merge pull request #80 from alanshaw/master
fix get address info for IPv4 addresses
2018-09-02 14:16:48 +01:00
Marek Ventur b26db572be
Merge pull request #81 from cclauss/patch-1
print() is a function in Python 3
2018-09-02 14:15:49 +01:00
Marek Ventur 1e08e758d0
Merge pull request #82 from 99wombats/warm_and_wet
First submission of temp and humidity sensor data display app
2018-09-02 14:15:21 +01:00
Marek Ventur f01e399ee4
Merge pull request #83 from davea/multitap-keypad
Multitap keypad text entry
2018-09-02 14:14:02 +01:00
Marek Ventur 0eaf8e34e7
Merge pull request #86 from tomsci/tomsci-load-flicker
Disable redraw while populating prompt_option list
2018-09-02 14:12:52 +01:00
mattbrejza 7e172664b6
Merge pull request #84 from danielsaul/master
Tildr: huge refactor.
2018-09-02 14:11:26 +01:00
Daniel Saul 3e39bdf912 Set bootstrapped false 2018-09-02 14:08:44 +01:00
Tom Sutcliffe 956ac9232d Disable redraw while populating prompt_option list
Reduces flicker in launcher app
2018-09-02 13:56:25 +01:00
Heinrich Mellmann b5666b1341 Revert "update test"
This reverts commit 7c27c018cf.
2018-09-02 13:36:31 +01:00
Dave Arter 60b320140d Multitap keypad text entry
For that old-skool text input experience
2018-09-02 13:31:42 +01:00
Daniel Saul 116613f424 Massive refactor: seperate files, screen state, dialog back, edit profile, local images 2018-09-02 13:28:54 +01:00
Heinrich Mellmann 7c27c018cf update test 2018-09-02 13:20:43 +01:00
99wombats dd787c9adf First submission of temp and humidity sensor data display app 2018-09-02 13:07:55 +01:00
Heinrich Mellmann 9d80d34ae6 exit on button 2018-09-02 13:52:42 +02:00
Heinrich Mellmann aab6260dbf a test image :) 2018-09-02 13:48:41 +02:00
cclauss 15bf640af4
print() is a function in Python 3 2018-09-02 13:30:33 +02:00
Duane Bekaert 68189a43d8 Added Avatar, a new homescreen grabbing an image from an URL, no local storage needed 2018-09-02 13:27:14 +02:00
Heinrich Mellmann 5612350063 main file for the serendipity app 2018-09-02 13:17:36 +02:00
Pez Cuckow 7ce50073fd 🎉 Flash colors on your screen 2018-09-02 12:10:51 +01:00
Philip Henning 0edf7aa1e7 Stratum 0 flavored homescreen 2018-09-02 13:09:03 +02:00
Benjamin Boortz cad13f8869 hello world imports fixed 2018-09-02 12:03:00 +01:00
Alan Shaw 958104e090
fix: really fix get_address_info
License: MIT
Signed-off-by: Alan Shaw <alan@tableflip.io>
2018-09-02 11:56:22 +01:00
Alan Shaw d430751202
fix: return host and port for IPv4 address
License: MIT
Signed-off-by: Alan Shaw <alan@tableflip.io>
2018-09-02 11:49:29 +01:00
Benjamin Boortz 13ff1942a7 hello world imports fixed 2018-09-02 11:17:23 +01:00
Benjamin Boortz 8053c115a6 hello world imports fixed 2018-09-02 11:12:30 +01:00
Benjamin Boortz 7ab76e6d1d hello world imports fixed 2018-09-02 10:55:45 +01:00
Benjamin Boortz ea116a72b4 closing implemented 2018-09-02 10:41:52 +01:00
David Miles b87954d7f5 Fix selection of default app 2018-09-02 10:23:30 +01:00
Dave Potts 278230ef8e Fixing #73 - sleep to actually sleep for the duration passed 2018-09-02 10:03:13 +01:00
Dave Potts 39a8fc63bc Fixing #73 - sleep to actually sleep for the duration passed 2018-09-02 10:00:46 +01:00
Marek Ventur 2aeb0434da
remove bootstrap 2018-09-02 09:42:08 +01:00
Marek Ventur 071d03915c
Merge pull request #69 from ThePhilStrongProject/master
Add EMF 2018 badge simulator
2018-09-02 09:41:46 +01:00
Daniel Saul d717f61037 Remove unused imports 2018-09-02 09:41:25 +01:00
Marek Ventur f1b3b04dce
don't bootstrap 2018-09-02 09:41:19 +01:00
Marek Ventur c9b4ed4659
Merge pull request #59 from fooforever/master
Add Custom Image Homescreen App
2018-09-02 09:40:42 +01:00
Marek Ventur acf85ce817
Make launchable 2018-09-02 09:39:51 +01:00
Marek Ventur 625e30ad70
Merge pull request #71 from MisguidedEmails/pride
Added pride flag homescreen
2018-09-02 09:39:15 +01:00
Marek Ventur 4afe544568
Merge pull request #60 from emfvillager/master
Add Lucky Melody Machine app
2018-09-02 09:37:21 +01:00
Marek Ventur f94dd666f0
Merge pull request #67 from Shahor/master
Add a memorable dictaphone
2018-09-02 09:36:50 +01:00
Marek Ventur 8b20d00b64
Merge pull request #62 from chetbox/breakout
Breakout game
2018-09-02 09:36:16 +01:00
Marek Ventur cdcfbf98ee
Merge pull request #61 from choppedpork/beer2.0
Beer 2.0
2018-09-02 09:35:59 +01:00
Marek Ventur 0145c0c8fb
Merge pull request #65 from danielsaul/master
First (hacky) version of Tildr, the emfcamp dating app
2018-09-02 09:35:33 +01:00
Marek Ventur 279216ce29
Merge pull request #68 from peterdroberts/master
Amateur Radio homescreen app
2018-09-02 09:34:45 +01:00
Marek Ventur 95ed1e6be0
Merge pull request #64 from adnathanail/patch-1
Fixed incorrect instruction
2018-09-02 09:33:27 +01:00
Marek Ventur 01d1d9ff2f
Merge pull request #70 from peterdroberts/clock
Clock
2018-09-02 09:33:05 +01:00
Marek Ventur 2e2a9e95fd
Merge pull request #72 from Davermouse/square_home
Add a home screen with spinning squares
2018-09-02 09:30:34 +01:00
David Miles cc0fb8a7fd Add a home screen with spinning squares 2018-09-02 07:09:26 +01:00
MisguidedEmails d188eef874 Added pride flag homescreen 2018-09-02 04:35:40 +01:00
Peter Roberts 09596af20e revert changes to ntp file 2018-09-02 03:58:19 +01:00
Peter Roberts 6cae17d3bb Revert unnecessary changes 2018-09-02 03:47:42 +01:00
Peter Roberts f2de291d9e Move to basic_clock so it doesn't cause module import errors 2018-09-02 03:40:17 +01:00
Peter Roberts 36b55653af Working clock 2018-09-02 02:53:37 +01:00
Peter Roberts 5eea1d7a34 Set up realtime clock 2018-09-02 02:37:18 +01:00
Peter Roberts 0604f3b380 Use correct functions in sleep 2018-09-02 02:15:02 +01:00
Peter Roberts 1d0c4bc874 Shell of time app 2018-09-02 02:09:37 +01:00
Peter Roberts da7fa998db Remove code that stops wifi connect working 2018-09-02 02:09:04 +01:00
Peter Roberts ce36fadd3e Fix dodgy tabbed indentation 2018-09-02 02:08:26 +01:00
Daniel Saul 36d8465471 Temporarily remove edit profile 2018-09-02 00:35:33 +01:00
Peter Roberts 5645bfc15c Remove unused code 2018-09-02 00:21:56 +01:00
Philip Strong fa16d916d9
Add EMF 2018 badge simulator 2018-09-02 00:20:57 +01:00
Peter Roberts ee4948c1da Actually add emf_ham logo 2018-09-02 00:20:50 +01:00
Peter Roberts d48f2f7ed1 Remove sponsors dependency 2018-09-02 00:11:33 +01:00
Alexandre Gaudencio 6cf52f61dd Add a memorable dictaphone 2018-09-02 00:11:12 +01:00
Peter Roberts 3d60f29f08 Correct boilerplate 2018-09-02 00:04:22 +01:00
Peter Roberts c2a5b38f8e Remove emf_ham.png from shared#g 2018-09-01 23:49:53 +01:00
Peter Roberts b03327e782 Fix bugs stopping running 2018-09-01 23:48:08 +01:00
Peter Roberts a2438ead82 Move logo 2018-09-01 23:47:52 +01:00
Peter Roberts a305dda8f1 Revert to original logo 2018-09-01 23:32:32 +01:00
Peter Roberts 3a242e0354 Fix name badge alignment 2018-09-01 23:10:51 +01:00
Peter Roberts 6fab0b6b8e basic emf ham logo 2018-09-01 22:37:27 +01:00
Daniel Saul 38805f3a69 First (hacky) version of Tildr, the emfcamp dating app 2018-09-01 22:35:04 +01:00
adnathanail 7a3a8c9868
Fixed incorrect instruction 2018-09-01 22:33:17 +01:00
Chetan Padia d78de8528d hack to fix ball getting stuck 2018-09-01 22:21:22 +01:00
Chetan Padia f22c2ef44b Breakout game 2018-09-01 22:01:41 +01:00
Simon Szumyłowicz 17b8fc59cc better description 2018-09-01 21:50:12 +01:00
Simon Szumyłowicz 0db4dfc04f beer app v2.0 2018-09-01 21:45:33 +01:00
EMF Villager 84170bf6f3 Merge remote-tracking branch 'emfcamp/master' 2018-09-01 21:50:52 +02:00
EMF Villager f621ffeda2 Add Lucky Melody Machine app 2018-09-01 21:49:19 +02:00
Nick Boone 57ca044848 Change description 2018-09-01 20:03:55 +01:00
Nick Boone 9b64a96acc Fix space in filename 2018-09-01 20:01:21 +01:00
Marek Ventur 7bfb7bef22
Merge pull request #43 from trandi/game-of-life
Game of life - 1st draft
2018-09-01 19:27:45 +01:00
Marek Ventur a94272d8f6
Merge pull request #49 from peteredhead/master
Adds Lobster Vision app
2018-09-01 19:27:12 +01:00
Marek Ventur fbc695238c
Merge pull request #50 from crimsoneer/master
LED Party added
2018-09-01 19:26:23 +01:00
Marek Ventur 7787afef15
Merge pull request #54 from liclac/dowsing-rod
WiFi Dowsing Rod
2018-09-01 19:25:55 +01:00
Marek Ventur f0cc9504d7
Merge pull request #56 from bboortz/hello_world
simple hello world app added
2018-09-01 19:24:46 +01:00
Marek Ventur 81675cfe7e
Merge pull request #53 from choppedpork/beer
super simple robot bar app!
2018-09-01 19:24:19 +01:00
Marek Ventur 80f9422360
Merge pull request #41 from prehensile/master
app to read stories from twentythreemillionstories.org
2018-09-01 19:23:47 +01:00
Marek Ventur 84388fdb27 This should not be in bootstrap 2018-09-01 19:21:53 +01:00
Marek Ventur 2fb02fa797
Merge pull request #52 from emfvillager/master
Add TiNDA app
2018-09-01 19:18:56 +01:00
Marek Ventur d2262ca62f Fix import 2018-09-01 19:14:35 +01:00
Marek Ventur 760e203456 Fix sponsor splash 2018-09-01 19:09:12 +01:00
Marek Ventur e683d50dc5 Add long press message to home_default 2018-09-01 18:53:55 +01:00
Marek Ventur 5dd21e49b4 Add review_helper app 2018-09-01 18:50:38 +01:00
Nick Boone 0fb34a9e73 Add Custom Image Homescreen App 2018-09-01 18:48:38 +01:00
Marek Ventur 1c06a8bbe4
Merge pull request #55 from hazanjon/snake_improvements
Fix Issue #38 - Reversing direction causes instant game over
2018-09-01 18:46:06 +01:00
Marek Ventur 2cd2ca468f
Merge pull request #58 from hazanjon/tools_windows_fix
tilda_tools - Fix "'charmap' codec can't decode byte 0x9d" on windows for validate, sync, run
2018-09-01 18:45:43 +01:00
Simon Szumyłowicz b212ef6906 add screen rotation 2018-09-01 18:28:59 +01:00
Marek Ventur 87c82fffb7 Delete bootstrap.py on sync 2018-09-01 18:27:03 +01:00
Jon Hazan f3e3c98211 Fix charmap error when using tilda_tools on windows 2018-09-01 18:25:04 +01:00
Marek Ventur adbb92ad57 Add wifi.json for the duration of emfg 2018-09-01 18:20:14 +01:00
Jon Hazan 1155953462 Fix charmap error when using tilda_tools on windows 2018-09-01 18:19:14 +01:00
Marek Ventur 0dec27757c Don't allow sync without filter 2018-09-01 18:17:56 +01:00
Marek Ventur aa6a80c269 Fix sync for windows users 2018-09-01 18:14:21 +01:00
Erin Shepherd 2bfbf9342d Make SSID customizable 2018-09-01 17:52:47 +01:00
Benjamin Boortz f6e27da0c7 simple hello world app added 2018-09-01 17:48:26 +01:00
Erin Shepherd f7f1fb9b9f Add no APs found message 2018-09-01 17:35:19 +01:00
Erin Shepherd acb5270f16 Toggle Wi-Fi enable to clear known network cache 2018-09-01 17:35:19 +01:00
liclac d46d66211c wifi dowsing rod 2018-09-01 17:35:19 +01:00
Jon Hazan f16351863f Fix Issue 38 - Reversing direction causes instant game over 2018-09-01 17:30:20 +01:00
Simon Szumyłowicz 31f85accb4 slightly nicer loading screen 2018-09-01 16:59:56 +01:00
Simon Szumyłowicz 53b8e7d654 now interrupt driven 2018-09-01 16:53:59 +01:00
Andreas Varotsis e75df63743 changed bootstrap
Bootstrap set to False
2018-09-01 16:53:50 +01:00
EMF Villager 4addf5bc85 Add TiNDA app 2018-09-01 17:52:37 +02:00
Marek Ventur 15de09127c
Merge pull request #51 from bingmann/master
sms: fix variable name num -> number
2018-09-01 16:51:28 +01:00
trandi ae565b3b68 Game of life - 1st draft 2018-09-01 16:46:59 +01:00
Kimball Johnson 9f39749f0f
Update Mass Storage App
Wait for one second before calling disable command
2018-09-01 16:45:43 +01:00
Andreas Varotsis 81a739a978 buttons enabled
Added input options and instructions to screen
2018-09-01 16:40:29 +01:00
Timo Bingmann 14a608787f sms: fix variable name num -> number 2018-09-01 17:34:56 +02:00
Alistair MacDonald 7526b4b9b6
Another quick fix 2018-09-01 16:24:58 +01:00
Alistair MacDonald 5a1ea3a7d4
Bug fixes 2018-09-01 16:15:35 +01:00
Andreas Varotsis 249edf2d41 LED Party added
Added first version of my LED Party app
2018-09-01 15:57:35 +01:00
Simon Szumyłowicz 924afb597b initial commit 2018-09-01 14:53:14 +01:00
Pete Redhead d63360c4ae Adds Lobster Vision app (viewer for timelapse camera recording site build) 2018-09-01 14:41:57 +01:00
Marek Ventur 3fd67bc82a
Update DFU instructions 2018-09-01 14:28:50 +01:00
Roger Monk 0c40c9ee1e sysinfo: make bootstrapped and show exit options 2018-09-01 13:43:14 +01:00
Marek Ventur 700ac49199
Merge pull request #44 from SteveAmor/patch-1
Add restart_to_default() to Mass Storage App
2018-09-01 13:27:53 +01:00
Roger Monk f10b67493d Add SysInfo App for testing sensors and showing status 2018-09-01 13:22:00 +01:00
Marek Ventur 7bfb22e782
Merge pull request #42 from Jake-E/patch-1
Fixed typo
2018-09-01 12:54:30 +01:00
Steveis 5fe53df593
Add restart_to_default() to Mass Storage App 2018-09-01 12:48:08 +01:00
Marek Ventur 6202754906 Shuffle sponsor logos 2018-09-01 12:07:23 +01:00
Jake Evans 1b82610578
Fixed typo 2018-09-01 11:55:10 +01:00
Alistair MacDonald f8102d5652
Fixed crash on operator select cancel 2018-09-01 11:17:01 +01:00
prehensile 4f8f9992ef fixed category 2018-09-01 11:08:24 +01:00
prehensile 8ef55254d8 fixed docstring 2018-09-01 11:05:51 +01:00
prehensile 388948b9e2 stories app, first decent version 2018-09-01 11:04:13 +01:00
Marek Ventur 3c0b33282a Sponsor splash 2018-09-01 10:48:20 +01:00
Alistair MacDonald 567219d590
DTMF dialling in call added 2018-08-31 20:33:21 +01:00
Alistair MacDonald 571b0307d8
More big fixing 2018-08-31 20:11:19 +01:00
Alistair MacDonald 0d6d62797c
Fit home[screen] button on button 2018-08-31 19:55:38 +01:00
Alistair MacDonald c87360cda7
Number look up fix 2018-08-31 19:54:40 +01:00
Alistair MacDonald dfc23b2ee5
Intto str bug fix 2018-08-31 19:45:50 +01:00
Alistair MacDonald d21afe3f59
Added lots more functionality 2018-08-31 19:33:02 +01:00
Dave Arter fbaf5fb306 Add websockets module 2018-08-31 19:20:59 +01:00
Alistair MacDonald 0fde084d3d
Start the SIM800 at load 2018-08-31 18:20:57 +01:00
Alistair MacDonald 8780933793
Added option to delete SMS 2018-08-31 18:18:52 +01:00
Marek Ventur 164764bf2c Make the badge store quitable 2018-08-31 18:11:25 +01:00
Marek Ventur 13b3399ea9 Snake - closes #9 2018-08-31 17:47:32 +01:00
Andrejus 48d03146a6
Merge pull request #37 from andrejusk/master
Stylise homescreen
2018-08-31 17:46:37 +01:00
Andrejus 89a4ee789d Merge branch 'feature/home' 2018-08-31 17:44:42 +01:00
Andrejus 3a2a40ea8f Stylise homescreen 2018-08-31 17:44:29 +01:00
Andrejus 7158366924 Implement getting battery from SIM800 2018-08-31 17:44:22 +01:00
Andrejus 35214f103e Add shared logo resource 2018-08-31 17:44:20 +01:00
Alistair MacDonald edcb6fccb0
Fix for crash on exiting app. 2018-08-31 17:43:14 +01:00
Lex Robinson 2f5e86c734
Merge pull request #25 from Lexicality/windows
Fix tilda_tools test not working in Windows
2018-08-31 17:20:30 +01:00
Lex Robinson 98e53077b6
Fix tilda_tools test not working in Windows
Fixes for hard coded '/'s.
2018-08-31 17:19:59 +01:00
Dave Arter a888d5a86a Settings app can be quit too 2018-08-31 17:05:14 +01:00
Dave Arter aff49d1ea7 Joystick center button can be clicked to select menu option 2018-08-31 17:04:45 +01:00
Dave Arter 66bb217412 Can now exit synth app and change octave 2018-08-31 16:48:27 +01:00
Marek Ventur b1960d34ae
Merge pull request #34 from davea/ensure_clean_restarts
Ensure apps call restart_to_default() when finished
2018-08-31 16:39:42 +01:00
Dave Arter a78c19d05c Ensure apps call restart_to_default() when finished 2018-08-31 16:20:30 +01:00
Marek Ventur 5d777f23da Badge Store: Update/Remove 2018-08-31 15:55:28 +01:00
Marek Ventur 02d3c1512b Show correct rssi strength on homescreen 2018-08-31 14:23:06 +01:00
dps.lwk 01d5c2dc5c
back to 3 as we have no time 2018-08-31 12:13:08 +01:00
dps.lwk 22f478ecc7
Fix Kimballs C in Python semicolons 2018-08-31 11:56:03 +01:00
Kimball Johnson d6c0318929 Send 2 lots of 2 neopixel zeros 2018-08-31 11:48:37 +01:00
Kimball Johnson b4768d0300 Turn of neopixels on boot 2018-08-30 23:30:26 +01:00
Alistair MacDonald 6be3300da0
Wait for the SIM800 to start if it has not already 2018-08-30 22:24:55 +01:00
Alistair MacDonald e97f61626b
Wait for the SIM800 to start if it has not already 2018-08-30 22:24:06 +01:00
Marek Ventur 63fb58dcdd Update boot(strap).py 2018-08-30 19:17:08 +01:00
Marek Ventur 6d303c554f
Merge pull request #31 from Floppy/homescreen-name-db-key
Fix homescreen name database key
2018-08-30 18:49:51 +01:00
Marek Ventur 780b1a2fc3
Merge pull request #32 from Floppy/dry-homescreen-name-rendering
DRY up the homescreen name rendering
2018-08-30 18:49:26 +01:00
James Smith d0c461cf2c
DRY up the rendering of the name on the homescreen 2018-08-30 18:43:00 +01:00
James Smith 1bb1205617
Fix homescreen name db key 2018-08-30 18:37:47 +01:00
Marek Ventur f993b5526e
Merge pull request #16 from philcrump/phil-add-ntp
Add NTP lib.
2018-08-30 18:07:32 +01:00
Marek Ventur 7b1b93e02b
Merge pull request #30 from philcrump/phil-fix-py36
firmware-update.py: Use py3.6 compatible arguments to subprocess.run()
2018-08-30 18:06:09 +01:00
Phil Crump 13dec9e50e firmware-update.py: Use py3.6 compatible arguments to subprocess.run() 2018-08-30 18:02:49 +01:00
Marek Ventur 1452c37375
Merge pull request #24 from Floppy/fix-settings-typo
Fix typo in settings menu
2018-08-30 17:59:39 +01:00
Marek Ventur b374986088
Merge pull request #28 from prehensile/master
added /dev/tty.usbmodemTiLDA* to glob paths in find_tty
2018-08-30 17:50:06 +01:00
prehensile be79970675 added /dev/tty.usbmodemTiLDA* to glob paths in find_tty 2018-08-30 15:03:52 +01:00
James Smith c9e79aff4a
Fix typo in settings menu (homescreeen) 2018-08-29 23:15:15 +01:00
Phil Crump 407c769d7b Add onsite ntp server, ntp vendor pool as NTP hosts. 2018-08-29 22:25:07 +01:00
Kimball Johnson 115837dd07 Fix typo in bootstrap.py 2018-08-29 22:05:20 +01:00
Marek Ventur 3234dbc39f
Merge pull request #18 from emfcamp/settings-app
Settings app
2018-08-29 21:30:56 +01:00
Marek Ventur 0f3c2c0e7c Settings app 2018-08-29 21:29:18 +01:00
Marek Ventur f7fefaa331 Fix wifi.py 2018-08-29 21:27:23 +01:00
Marek Ventur ef6ca172df
Merge pull request #22 from emfcamp/tildatorch
Tilda Torch app for workshop
2018-08-29 21:25:13 +01:00
Bob Clough cb2c2743b8 Tilda Torch app for workshop 2018-08-29 21:08:33 +01:00
Alistair MacDonald 642b4a0d16
Added SMS sending 2018-08-29 19:58:04 +01:00
Marek Ventur f7ffa0e99a
Merge pull request #23 from SteveAmor/PRs
update contributing.md
2018-08-29 19:53:49 +01:00
steveamor 73c6a1fc20 update contributing.md 2018-08-29 19:00:56 +01:00
Marek Ventur ea8fd1374e
Update README 2018-08-29 17:53:39 +01:00
Marek Ventur a0d1d95b45 Fix boot.py 2018-08-29 15:36:45 +01:00
Marek Ventur 21560c049e Add wpa enterprise to bootstrap.py/wifi.py 2018-08-29 15:06:51 +01:00
Marek Ventur 8505884b07 Add mass storage enabling app 2018-08-29 13:06:54 +01:00
Marek Ventur 3d42f7458f Soft-reset before syncing 2018-08-29 13:06:36 +01:00
Marek Ventur 4fb8205cab Fix tests, remove debug leftovers 2018-08-29 12:39:09 +01:00
Marek Ventur be985b3240 fix tilda-tools bootstrap 2018-08-29 12:39:09 +01:00
Marek Ventur 1ef5e7786f Fix category name of home_default 2018-08-29 12:39:09 +01:00
Alistair MacDonald e2c33faf79
Small bug fix relating to interrupts afecting file transfer 2018-08-29 12:14:03 +01:00
Alistair MacDonald 916a3b054c
Added keypad entry in to text prompt 2018-08-29 12:06:18 +01:00
Marek Ventur 5611b00a6c
Create CONTRIBUTING 2018-08-29 11:49:25 +01:00
Marek Ventur 7a32fe1158
Merge pull request #21 from SteveAmor/patch-1
add sleep.wfi() to sponsors app
2018-08-29 09:50:40 +01:00
Marek Ventur 9010b5b282
Merge pull request #20 from emfcamp/sim800-lib-and-apps
Big fix
2018-08-29 09:49:57 +01:00
Alistair MacDonald 1d02747ec9
Swapped button A for Menu in some for consistancy 2018-08-29 09:39:26 +01:00
Steveis 258aab08a8
add sleep.wfi()
Change ```time.sleep(0.01)``` to ```sleep.wfi()``` to save battery power when sleep.wfi() is fixed.  Ironically, this change will increase battery load until sleep.wfi() is implemented properly.  But it is in line with the instructions here 69c7cd39b5/lib/homescreen.py (L9)
2018-08-29 08:07:51 +01:00
Alistair MacDonald 52b28bbc1f
Start the SIM800 at load 2018-08-28 23:44:23 +01:00
Alistair MacDonald 781eff06cc
Trying to fix headers 2018-08-28 23:35:49 +01:00
Alistair MacDonald 5c528d1411
Trying to fix headers 2018-08-28 23:35:45 +01:00
Alistair MacDonald 2399c9c589
Fix header for MK4 2018-08-28 23:28:58 +01:00
Alistair MacDonald 87053ff323
Basic phone app for testing
This needs work but can dial a number.
2018-08-28 23:23:38 +01:00
Alistair MacDonald ac4d65d748
Basic SMS reader app 2018-08-28 23:12:57 +01:00
Alistair MacDonald 81441b1b4d
Big fix
Don't check for callbacks while running a command.
2018-08-28 22:49:14 +01:00
Alistair MacDonald 6cd5d3e16b
Minor bug fix
Quick fix for network selection.
2018-08-28 21:42:41 +01:00
Alistair MacDonald 6c79e6fc97
Merge pull request #19 from emfcamp/sim800-lib-and-apps
Minor fixes for issues with latest firmware. Added interrupt for callbacks.
2018-08-28 20:55:16 +01:00
Alistair MacDonald 9b7afad5c9
Bug fixing
Small issue with interrupts and loading when SIM800 is already on.
2018-08-28 20:51:02 +01:00
Alistair MacDonald be1032ffd2
Added interrupt for monitoring uart and buttons. 2018-08-28 20:08:41 +01:00
Alistair MacDonald 319f45bb11
Minor bug fixes
Venerable name changes for compatibility with new. Turn on amp. Worked around SMS unicode translation issue.
2018-08-28 19:23:02 +01:00
Alistair MacDonald fbc2d1a348
Final tweaks or tidying 2018-08-28 12:56:18 +01:00
Alistair MacDonald eb3ff46b88
Bluetooth SSP
Note that is needs testing.
2018-08-27 23:41:33 +01:00
Alistair MacDonald 2293aeb5f3
OPP transfer and other Bluetooth tidying 2018-08-27 23:06:23 +01:00
Marek Ventur 3b6f660f13
Merge pull request #17 from emfcamp/philcrump-phil-add-ntp
NTP #2
2018-08-27 20:36:35 +01:00
Marek Ventur d0a133c978 Add sponsors app 2018-08-27 17:25:54 +01:00
Marek Ventur 7997abc090 Some fixes and added tests 2018-08-27 16:50:34 +01:00
Phil Crump eb1fa19603 Add NTP lib. 2018-08-27 16:29:08 +01:00
Marek Ventur cd38a55af6 Fix badge store (it was pointing at the wrong url) 2018-08-27 16:28:38 +01:00
Marek Ventur ce22a9f176 Fix http library (mostly) 2018-08-27 16:07:47 +01:00
Marek Ventur a31d48749e Here be dragons: Don't use usb storage for syncing anymore, switch to REPL only 2018-08-27 16:06:04 +01:00
Marek Ventur 6564ac3681 Add wifi select UI to bootstrap 2018-08-27 10:02:16 +01:00
Alistair MacDonald a5dcc4babb
Bug fixing, optimising and tidying 2018-08-27 00:22:23 +01:00
Alistair MacDonald 03400de0db
Binary file transfer and audio recording 2018-08-26 20:57:26 +01:00
Marek Ventur 69c7cd39b5 Bootstrap 2018-08-26 20:27:23 +01:00
Alistair MacDonald aad89b4777
Added more engineering and debug functions 2018-08-26 18:40:01 +01:00
Alistair MacDonald 41a2fa37dd
Callback for ringing, CLI and SMS added. Tidied turn on and command processes for better asynchronous startup. 2018-08-26 17:37:19 +01:00
Alistair MacDonald 655af11946
Correcting Spelling 2018-08-26 12:20:33 +01:00
Alistair MacDonald 77680a45c6
Filesystem access added 2018-08-26 12:11:11 +01:00
Marek Ventur 63c685dc20 Update dialogs to ugfx changes 2018-08-26 11:39:04 +01:00
Marek Ventur 98f5a0507d Travis didn't like my last commit 2018-08-25 22:56:35 +01:00
Marek Ventur 0f168e6053 Inform users to install pyserial 2018-08-25 22:43:23 +01:00
Marek Ventur d640329911 bootstrap.py + switch to badgeserver 2018-08-25 16:52:30 +01:00
Alistair MacDonald 6490ebb885
More Bluetooth functionality 2018-08-25 10:25:58 +01:00
Marek Ventur 14007f3d9e Random fixes 2018-08-24 23:00:09 +01:00
Alistair MacDonald ab91fcece0
Bluetooth pairing and connection 2018-08-24 15:03:24 +01:00
Alistair MacDonald f25369fdff
More Bluetooth added and other tidying 2018-08-24 12:57:25 +01:00
Alistair MacDonald 73501c4c62
Merge pull request #2 from emfcamp/sammachin-get-imsi
Added function to get the IMSI
2018-08-24 09:58:55 +01:00
Sam Machin 2904a08d5e
Added function to get the IMSI
Getting the IMSI is useful for trying to debug issues with peoples subscriptions  in the GSM netwotk and its not printed on the card or anywhere, if the card isn't inserted or hasn't started properly then the command returns ERROR
2018-08-24 08:57:59 +01:00
Marek Ventur ba94e340f8 fix wifi test 2018-08-23 23:11:57 +01:00
Marek Ventur 5b8189a015 Bootstrap 'app' 2018-08-23 23:07:25 +01:00
Alistair MacDonald 965c36e439
Quick typo fix 2018-08-22 23:51:11 +01:00
Alistair MacDonald ff81bbca94
Quick SMS fix
Force text mode on all SMS request just in case someone is issuing direct +CMGF commands from an app.
2018-08-22 16:18:59 +01:00
Alistair MacDonald 816f62ae96
Added getfirmwarever() 2018-08-22 16:02:32 +01:00
Alistair MacDonald 77fa55bb95
Added Bluetooth power, status and name/address 2018-08-22 15:53:57 +01:00
Alistair MacDonald beec1705bc
Finished call, sms and basic functionality 2018-08-22 15:12:12 +01:00
Alistair MacDonald 1657999162
Fixing stupid filename typo. 2018-08-22 13:49:57 +01:00
Alistair MacDonald f900bb320f
Further command updates
More commands added. A few efficiency updates. command() not accepts non AT commands. Timeouts localised. Non blocking power on/off option now available.
2018-08-22 13:48:42 +01:00
Marek Ventur 68e0c97cbf Keep resources.py in sync with backend 2018-08-21 21:00:42 +01:00
Marek Ventur ae7c18d062 This is getting too big, so commiting now: More Tilda mk4 changes 2018-08-21 20:47:39 +01:00
Alistair MacDonald f2a3b23872
Initial upload of SIM800 library
Base command processing is done. A selecting of commands implemented (mostly call and network).
2018-08-19 18:14:37 +01:00
Marek Ventur 42c27b9ce2 First pass at making libs work with Mk4 2018-08-18 22:18:22 +01:00
Marek Ventur 80e1e03446 Update firmware updater for TiLDA Mk4. Sadly I had to take a dependency on dfu-util though 2018-08-12 17:53:11 +01:00
Marek Ventur 72e124eb5b random lib and homescreen improvements 2018-08-07 22:41:33 +01:00
Marek Ventur 8904484cb9 improve homescreen lib 2018-08-06 21:45:35 +01:00
Marek Ventur 7936cbf3a7 Launcher and minimal home screen 2018-08-05 22:14:31 +01:00
Marek Ventur 1c9e95db23 Working badge store 2018-08-02 22:42:45 +01:00
Marek Ventur b4da6f49f0 Badge store lib 2018-08-02 21:33:29 +01:00
197 changed files with 18392 additions and 1069 deletions

View File

@ -0,0 +1,81 @@
# This is meant to run on the badge
import hashlib, binascii, os
def split(path):
if path == "":
return ("", "")
r = path.rsplit("/", 1)
if len(r) == 1:
return ("", path)
head = r[0]
if not head:
head = "/"
return (head, r[1])
def dirname(path):
return split(path)[0]
def exists(path):
try:
os.stat(path)[0]
return True
except OSError:
return False
def makedirs(path):
sub_path = split(path)[0]
if sub_path and (not exists(sub_path)):
makedirs(sub_path)
if not exists(path):
os.mkdir(path)
def isdir(path):
try:
return os.stat(path)[0] & 0o170000 == 0o040000
except OSError:
return False
def h(p):
try:
with open(p, "rb") as f:
h = hashlib.sha256()
h.update(f.read())
print(str(binascii.hexlify(h.digest()), "utf8")[:10])
return
except:
pass
print("nooooooooo")
def w(p, c):
try:
print("file", p)
makedirs(dirname(p))
with open(p, "wb") as f:
f.write(binascii.a2b_base64(c))
os.sync()
print("OK")
except Exception as e:
import sys
print("Error while writing file %s" % p)
sys.print_exception(e)
pass
def clean(path=""):
for s in os.listdir(path):
full = "/".join([path, s]) if path else s
try:
if isdir(full):
try:
clean(full)
except:
pass
os.rmdir(full)
else:
os.remove(full)
except Exception as e:
print("Error while trying to clean '%s'" % full)
try:
os.remove("bootstrap.py")
except:
pass

View File

@ -0,0 +1,58 @@
import urllib.request, tempfile, os, shutil, subprocess
def firmware_update(verbose):
global __verbose
__verbose = verbose
temp_path = tempfile.mktemp("firmware.dfu")
url = "https://s3.amazonaws.com/tilda-badge/mk4/firmware.dfu"
device = "1cbe:00ff"
print("Hello - Welcome to the automated TiLDA Mk4 firmware updater")
try:
response = subprocess.run(["dfu-util", "--list"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if response.returncode != 0:
print(response)
return
if ("Found DFU: [%s]" % device) not in response.stdout.decode('utf-8'):
print(response.stdout.decode('utf-8'))
print("We couldn't find a DFU enabled badge. Please check the following:")
print("")
print("1) Your badge is plugged into this computer via USB")
print("2) The switch underneath the screen at the back of the badge is set to 'on'")
print("3) Your badge is in DFU mode. You can tell by a small, red flashing light at the back")
print("")
print("To put your badge into DFU mode (or if you're unsure whether it really is) you need to")
print("press the joystick centrally down while pressing the reset button at the back.")
print("")
print("After that, please try this script again.")
return
print("Downloading newest firmware: ", end="", flush=True)
with urllib.request.urlopen(url) as response:
with open(temp_path, 'wb') as tmp_file:
shutil.copyfileobj(response, tmp_file)
print("DONE")
response = subprocess.run(["dfu-util", "--download", temp_path])
if response.returncode != 0:
print("Something went wrong during DFU updload :(")
print("")
print(response)
return
print("")
print("You can now restart your badge by pressing the reset button on the back. Please follow the instructions on the screen to finish the setup")
print("Have a nice day!")
except FileNotFoundError as e:
if "No such file or directory: 'dfu-utils'" in str(e):
print("We couldn't find dfu-util. You might have to install it.")
print("You can find instructions here: http://dfu-util.sourceforge.net/")
print("Please try again after you've installed dfu-util.")
else:
raise e
finally:
if os.path.isfile(temp_path): os.remove(temp_path)

View File

@ -240,7 +240,7 @@ class Pyboard:
delayed = False
for attempt in range(wait + 1):
try:
self.serial = serial.Serial(device, baudrate=baudrate, interCharTimeout=1)
self.serial = serial.Serial(device, baudrate=baudrate, interCharTimeout=1, timeout=1, write_timeout=1)
break
except (OSError, IOError): # Py2 and Py3 have different errors
if wait == 0:
@ -282,9 +282,8 @@ class Pyboard:
time.sleep(0.01)
return data
def enter_raw_repl(self):
def enter_raw_repl(self, retry_count = 2):
self.serial.write(b'\r\x03\x03') # ctrl-C twice: interrupt any running program
# flush input (without relying on serial.flushInput())
n = self.serial.inWaiting()
while n > 0:
@ -295,6 +294,8 @@ class Pyboard:
data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n>')
if not data.endswith(b'raw REPL; CTRL-B to exit\r\n>'):
print(data)
if retry_count:
self.enter_raw_repl(retry_count - 1)
raise PyboardError('could not enter raw repl')
self.serial.write(b'\x04') # ctrl-D: soft reset
@ -309,7 +310,7 @@ class Pyboard:
print(data)
raise PyboardError('could not enter raw repl')
def exit_raw_repl(self):
def exit_raw_repl(self):\
self.serial.write(b'\r\x02') # ctrl-B: enter friendly REPL
def follow(self, timeout, data_consumer=None):

View File

@ -1,12 +1,12 @@
from pyboard import Pyboard, PyboardError
import glob, sys, pyboard
import glob, sys, pyboard, json, binascii, os, hashlib
_pyb = None
def get_pyb(args):
global _pyb
if not _pyb:
print("Connected to badge:", end="")
print("Connected to badge:", end="", flush=True)
if not args.device:
args.device = find_tty()
@ -27,11 +27,13 @@ def close_pyb():
def stop_badge(args, verbose):
pyb = get_pyb(args)
if verbose:
print("Stopping running app:", end="")
print("Stopping running app:", end="", flush=True)
write_command(pyb, b'\r\x03\x03') # ctrl-C twice: interrupt any running program
if verbose:
print(" DONE")
n = pyb.serial.inWaiting()
while n > 0:
pyb.serial.read(n)
n = pyb.serial.inWaiting()
print(" DONE")
def write_command(pyb, command):
flush_input(pyb)
@ -47,11 +49,9 @@ def flush_input(pyb):
def soft_reset(args, verbose = True):
pyb = get_pyb(args)
if verbose:
print("Soft reboot:", end="")
print("Soft reboot:", end="", flush=True)
write_command(pyb, b'\x04') # ctrl-D: soft reset
#print("1")
data = pyb.read_until(1, b'soft reboot\r\n')
#print("2")
if data.endswith(b'soft reboot\r\n'):
if verbose:
print(" DONE")
@ -61,24 +61,70 @@ def soft_reset(args, verbose = True):
raise PyboardError('could not soft reboot')
def find_tty():
# Todo: find solution for windows, test in linux
for pattern in ['/dev/ttyACM*', '/dev/tty.usbmodem*']:
# Todo: test in linux, let user pick if multiple ports are available
for pattern in ['/dev/ttyACM*', '/dev/tty.usbmodemTiLDA*', '/dev/tty.usbmodem*']:
for path in glob.glob(pattern):
return path
if sys.platform.startswith('win'):
import serial
for port in ['COM%s' % (i + 1) for i in range(256)]:
try:
s = serial.Serial(port)
s.close()
return port
except (OSError, serial.SerialException):
pass
print("Couldn't find badge tty - Please make it's plugged in and reset it if necessary")
sys.exit(1)
def check_run(paths):
for filename in paths:
with open(filename, 'r') as f:
with open(filename, 'r', encoding='utf8') as f:
pyfile = f.read()
compile(pyfile + '\n', filename, 'exec')
def execbuffer(pyb, buf):
try:
ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=pyboard.stdout_write_bytes)
except PyboardError as er:
print(er)
pyb.close()
sys.exit(1)
except KeyboardInterrupt:
sys.exit(1)
if ret_err:
pyb.exit_raw_repl()
pyb.close()
pyboard.stdout_write_bytes(ret_err)
sys.exit(1)
def returnbuffer(pyb, buf):
res = b''
def add_to_res(b):
nonlocal res
res += b
try:
ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=add_to_res)
except PyboardError as er:
print(er)
pyb.close()
sys.exit(1)
except KeyboardInterrupt:
sys.exit(1)
if ret_err:
pyb.exit_raw_repl()
pyb.close()
pyboard.stdout_write_bytes(ret_err)
sys.exit(1)
return res.decode('ascii').strip()
def run(args, paths, verbose=True):
pyb = get_pyb(args)
if verbose:
print("Preparing execution:", end="")
print("Preparing execution:", end=" ", flush=True)
# run any command or file(s) - this is mostly a copy from pyboard.py
if len(paths):
# we must enter raw-REPL mode to execute commands
@ -94,27 +140,98 @@ def run(args, paths, verbose=True):
if verbose:
print(" DONE")
def execbuffer(buf):
try:
ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=pyboard.stdout_write_bytes)
except PyboardError as er:
print(er)
pyb.close()
sys.exit(1)
except KeyboardInterrupt:
sys.exit(1)
if ret_err:
pyb.exit_raw_repl()
pyb.close()
pyboard.stdout_write_bytes(ret_err)
sys.exit(1)
try:
# run any files
for filename in paths:
with open(filename, 'rb') as f:
print("-------- %s --------" % filename)
pyfile = f.read()
execbuffer(pyb, pyfile)
# run any files
for filename in paths:
with open(filename, 'rb') as f:
print("-------- %s --------" % filename)
pyfile = f.read()
execbuffer(pyfile)
# exiting raw-REPL just drops to friendly-REPL mode
pyb.exit_raw_repl()
except OSError as e:
if "Device not configured" in str(e):
print("Connection to badge lost") # This can happen on a hard rest
else:
raise e
# Please don't judge me too harshly for this hack, I had lots of problems with the
# USB mass storage protocol and at some point it looked simpler to just avoid it
# altogether. This _seems_ to work, so maybe it isn't that terrible after all.
def init_copy_via_repl(args):
pyb = get_pyb(args)
print("Init copy via repl:", end=" ", flush=True)
try:
pyb.enter_raw_repl()
with open(os.path.join(os.path.dirname(__file__), "copy_via_repl_header.py"), "rt") as f:
execbuffer(pyb, f.read())
except PyboardError as er:
print("FAIL")
print(er)
pyb.close()
sys.exit(1)
print("DONE")
def copy_via_repl(args, path, rel_path):
with open(path, "rb") as f:
return write_via_repl(args, f.read(), rel_path)
def write_via_repl(args, content, rel_path):
pyb = get_pyb(args)
h = hashlib.sha256()
h.update(content)
content = binascii.b2a_base64(content).decode('ascii').strip()
if os.sep != '/':
rel_path = rel_path.replace(os.sep, '/')
rel_path_as_string = json.dumps(rel_path) # make sure quotes are escaped
cmd = "h(%s)" % rel_path_as_string
badge_hash = returnbuffer(pyb,cmd).splitlines()[0]
local_hash = str(binascii.hexlify(h.digest()), "utf8")[:10]
if badge_hash == local_hash:
# we don't need to update those files
return False
cmd = "w(%s, \"%s\")\n" % (rel_path_as_string, content)
result = returnbuffer(pyb,cmd)
if "OK" in result:
return True
raise Exception("Couldn't write %s to badge: %s" % (rel_path, result))
def end_copy_via_repl(args):
# do we need to do anything?
pass
def clean_via_repl(args):
init_copy_via_repl(args)
print("Cleaning:", end=" ", flush=True)
try:
execbuffer(get_pyb(args), "clean()")
except PyboardError as er:
print("FAIL")
print(er)
pyb.close()
sys.exit(1)
print("DONE")
def hard_reset(args):
pyb = get_pyb(args)
print("Hard reset:", end=" ", flush=True)
try:
pyb.enter_raw_repl()
execbuffer(pyb, "import machine\nmachine.reset()\n")
print("UNEXPECTED")
except PyboardError as er:
print("FAIL")
print(er)
pyb.close()
sys.exit(1)
except Exception as e:
if "Errno 6" in str(e):
print("DONE")
else:
raise e
# exiting raw-REPL just drops to friendly-REPL mode
pyb.exit_raw_repl()

View File

@ -1,543 +0,0 @@
#!/usr/bin/env python
# This file is part of the OpenMV project.
# Copyright (c) 2013/2014 Ibrahim Abdelkader <i.abdalkader@gmail.com>
# This work is licensed under the MIT license, see the file LICENSE for
# details.
"""This module implements enough functionality to program the STM32F4xx over
DFU, without requiring dfu-util.
See app note AN3156 for a description of the DFU protocol.
See document UM0391 for a dscription of the DFuse file.
"""
from __future__ import print_function
import argparse
import re
import struct
import sys
import usb.core
import usb.util
import zlib
# VID/PID
__VID = 0x0483
__PID = 0xdf11
# USB request __TIMEOUT
__TIMEOUT = 4000
# DFU commands
__DFU_DETACH = 0
__DFU_DNLOAD = 1
__DFU_UPLOAD = 2
__DFU_GETSTATUS = 3
__DFU_CLRSTATUS = 4
__DFU_GETSTATE = 5
__DFU_ABORT = 6
# DFU status
__DFU_STATE_APP_IDLE = 0x00
__DFU_STATE_APP_DETACH = 0x01
__DFU_STATE_DFU_IDLE = 0x02
__DFU_STATE_DFU_DOWNLOAD_SYNC = 0x03
__DFU_STATE_DFU_DOWNLOAD_BUSY = 0x04
__DFU_STATE_DFU_DOWNLOAD_IDLE = 0x05
__DFU_STATE_DFU_MANIFEST_SYNC = 0x06
__DFU_STATE_DFU_MANIFEST = 0x07
__DFU_STATE_DFU_MANIFEST_WAIT_RESET = 0x08
__DFU_STATE_DFU_UPLOAD_IDLE = 0x09
__DFU_STATE_DFU_ERROR = 0x0a
_DFU_DESCRIPTOR_TYPE = 0x21
# USB device handle
__dev = None
__verbose = None
# USB DFU interface
__DFU_INTERFACE = 0
import inspect
if 'length' in inspect.getfullargspec(usb.util.get_string).args:
# PyUSB 1.0.0.b1 has the length argument
def get_string(dev, index):
return usb.util.get_string(dev, 255, index)
else:
# PyUSB 1.0.0.b2 dropped the length argument
def get_string(dev, index):
return usb.util.get_string(dev, index)
def init():
"""Initializes the found DFU device so that we can program it."""
global __dev
devices = get_dfu_devices(idVendor=__VID, idProduct=__PID)
if not devices:
raise ValueError('No DFU device found')
if len(devices) > 1:
raise ValueError("Multiple DFU devices found")
__dev = devices[0]
__dev.set_configuration()
# Claim DFU interface
usb.util.claim_interface(__dev, __DFU_INTERFACE)
# Clear status
clr_status()
def clr_status():
"""Clears any error status (perhaps left over from a previous session)."""
__dev.ctrl_transfer(0x21, __DFU_CLRSTATUS, 0, __DFU_INTERFACE,
None, __TIMEOUT)
def get_status():
"""Get the status of the last operation."""
stat = __dev.ctrl_transfer(0xA1, __DFU_GETSTATUS, 0, __DFU_INTERFACE,
6, 20000)
# print (__DFU_STAT[stat[4]], stat)
return stat[4]
def mass_erase():
"""Performs a MASS erase (i.e. erases the entire device."""
# Send DNLOAD with first byte=0x41
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE,
"\x41", __TIMEOUT)
# Execute last command
if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
raise Exception("DFU: erase failed")
# Check command state
if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
raise Exception("DFU: erase failed")
def page_erase(addr):
"""Erases a single page."""
if __verbose:
print("Erasing page: 0x%x..." % (addr))
# Send DNLOAD with first byte=0x41 and page address
buf = struct.pack("<BI", 0x41, addr)
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE, buf, __TIMEOUT)
# Execute last command
if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
raise Exception("DFU: erase failed")
# Check command state
if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
raise Exception("DFU: erase failed")
def set_address(addr):
"""Sets the address for the next operation."""
# Send DNLOAD with first byte=0x21 and page address
buf = struct.pack("<BI", 0x21, addr)
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE, buf, __TIMEOUT)
# Execute last command
if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
raise Exception("DFU: set address failed")
# Check command state
if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
raise Exception("DFU: set address failed")
def write_memory(addr, buf, progress=None, progress_addr=0, progress_size=0):
"""Writes a buffer into memory. This routine assumes that memory has
already been erased.
"""
xfer_count = 0
xfer_bytes = 0
xfer_total = len(buf)
xfer_base = addr
while xfer_bytes < xfer_total:
if __verbose and xfer_count % 512 == 0:
print ("Addr 0x%x %dKBs/%dKBs..." % (xfer_base + xfer_bytes,
xfer_bytes // 1024,
xfer_total // 1024))
if progress and xfer_count % 2 == 0:
progress(progress_addr, xfer_base + xfer_bytes - progress_addr,
progress_size)
# Set mem write address
set_address(xfer_base+xfer_bytes)
# Send DNLOAD with fw data
# the "2048" is the DFU transfer size supported by the ST DFU bootloader
# TODO: this number should be extracted from the USB config descriptor
chunk = min(2048, xfer_total-xfer_bytes)
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 2, __DFU_INTERFACE,
buf[xfer_bytes:xfer_bytes + chunk], __TIMEOUT)
# Execute last command
if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
raise Exception("DFU: write memory failed")
# Check command state
if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
raise Exception("DFU: write memory failed")
xfer_count += 1
xfer_bytes += chunk
def write_page(buf, xfer_offset):
"""Writes a single page. This routine assumes that memory has already
been erased.
"""
xfer_base = 0x08000000
# Set mem write address
set_address(xfer_base+xfer_offset)
# Send DNLOAD with fw data
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 2, __DFU_INTERFACE, buf, __TIMEOUT)
# Execute last command
if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
raise Exception("DFU: write memory failed")
# Check command state
if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
raise Exception("DFU: write memory failed")
if __verbose:
print ("Write: 0x%x " % (xfer_base + xfer_offset))
def exit_dfu():
"""Exit DFU mode, and start running the program."""
# set jump address
set_address(0x08000000)
# Send DNLOAD with 0 length to exit DFU
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE,
None, __TIMEOUT)
try:
# Execute last command
if get_status() != __DFU_STATE_DFU_MANIFEST:
print("Failed to reset device")
# Release device
usb.util.dispose_resources(__dev)
except:
pass
def named(values, names):
"""Creates a dict with `names` as fields, and `values` as values."""
return dict(zip(names.split(), values))
def consume(fmt, data, names):
"""Parses the struct defined by `fmt` from `data`, stores the parsed fields
into a named tuple using `names`. Returns the named tuple, and the data
with the struct stripped off."""
size = struct.calcsize(fmt)
return named(struct.unpack(fmt, data[:size]), names), data[size:]
def cstring(string):
"""Extracts a null-terminated string from a byte array."""
return string.decode('utf-8').split('\0', 1)[0]
def compute_crc(data):
"""Computes the CRC32 value for the data passed in."""
return 0xFFFFFFFF & -zlib.crc32(data) - 1
def read_dfu_file(filename):
"""Reads a DFU file, and parses the individual elements from the file.
Returns an array of elements. Each element is a dictionary with the
following keys:
num - The element index
address - The address that the element data should be written to.
size - The size of the element ddata.
data - The element data.
If an error occurs while parsing the file, then None is returned.
"""
print("File: {}".format(filename))
with open(filename, 'rb') as fin:
data = fin.read()
crc = compute_crc(data[:-4])
elements = []
# Decode the DFU Prefix
#
# <5sBIB
# < little endian
# 5s char[5] signature "DfuSe"
# B uint8_t version 1
# I uint32_t size Size of the DFU file (not including suffix)
# B uint8_t targets Number of targets
dfu_prefix, data = consume('<5sBIB', data,
'signature version size targets')
print (" %(signature)s v%(version)d, image size: %(size)d, "
"targets: %(targets)d" % dfu_prefix)
for target_idx in range(dfu_prefix['targets']):
# Decode the Image Prefix
#
# <6sBI255s2I
# < little endian
# 6s char[6] signature "Target"
# B uint8_t altsetting
# I uint32_t named bool indicating if a name was used
# 255s char[255] name name of the target
# I uint32_t size size of image (not incl prefix)
# I uint32_t elements Number of elements in the image
img_prefix, data = consume('<6sBI255s2I', data,
'signature altsetting named name '
'size elements')
img_prefix['num'] = target_idx
if img_prefix['named']:
img_prefix['name'] = cstring(img_prefix['name'])
else:
img_prefix['name'] = ''
print(' %(signature)s %(num)d, alt setting: %(altsetting)s, '
'name: "%(name)s", size: %(size)d, elements: %(elements)d'
% img_prefix)
target_size = img_prefix['size']
target_data, data = data[:target_size], data[target_size:]
for elem_idx in range(img_prefix['elements']):
# Decode target prefix
# < little endian
# I uint32_t element address
# I uint32_t element size
elem_prefix, target_data = consume('<2I', target_data, 'addr size')
elem_prefix['num'] = elem_idx
print(' %(num)d, address: 0x%(addr)08x, size: %(size)d'
% elem_prefix)
elem_size = elem_prefix['size']
elem_data = target_data[:elem_size]
target_data = target_data[elem_size:]
elem_prefix['data'] = elem_data
elements.append(elem_prefix)
if len(target_data):
print("target %d PARSE ERROR" % target_idx)
# Decode DFU Suffix
# < little endian
# H uint16_t device Firmware version
# H uint16_t product
# H uint16_t vendor
# H uint16_t dfu 0x11a (DFU file format version)
# 3s char[3] ufd 'UFD'
# B uint8_t len 16
# I uint32_t crc32
dfu_suffix = named(struct.unpack('<4H3sBI', data[:16]),
'device product vendor dfu ufd len crc')
print (' usb: %(vendor)04x:%(product)04x, device: 0x%(device)04x, '
'dfu: 0x%(dfu)04x, %(ufd)s, %(len)d, 0x%(crc)08x' % dfu_suffix)
if crc != dfu_suffix['crc']:
print("CRC ERROR: computed crc32 is 0x%08x" % crc)
return
data = data[16:]
if data:
print("PARSE ERROR")
return
return elements
class FilterDFU(object):
"""Class for filtering USB devices to identify devices which are in DFU
mode.
"""
def __call__(self, device):
for cfg in device:
for intf in cfg:
return (intf.bInterfaceClass == 0xFE and
intf.bInterfaceSubClass == 1)
def get_dfu_devices(*args, **kwargs):
"""Returns a list of USB device which are currently in DFU mode.
Additional filters (like idProduct and idVendor) can be passed in to
refine the search.
"""
# convert to list for compatibility with newer pyusb
return list(usb.core.find(*args, find_all=True,
custom_match=FilterDFU(), **kwargs))
def get_memory_layout(device):
"""Returns an array which identifies the memory layout. Each entry
of the array will contain a dictionary with the following keys:
addr - Address of this memory segment
last_addr - Last address contained within the memory segment.
size - size of the segment, in bytes
num_pages - number of pages in the segment
page_size - size of each page, in bytes
"""
cfg = device[0]
intf = cfg[(0, 0)]
mem_layout_str = get_string(device, intf.iInterface)
mem_layout = mem_layout_str.split('/')
result = []
for mem_layout_index in range(1, len(mem_layout), 2):
addr = int(mem_layout[mem_layout_index], 0)
segments = mem_layout[mem_layout_index + 1].split(',')
seg_re = re.compile(r'(\d+)\*(\d+)(.)(.)')
for segment in segments:
seg_match = seg_re.match(segment)
num_pages = int(seg_match.groups()[0], 10)
page_size = int(seg_match.groups()[1], 10)
multiplier = seg_match.groups()[2]
if multiplier == 'K':
page_size *= 1024
if multiplier == 'M':
page_size *= 1024 * 1024
size = num_pages * page_size
last_addr = addr + size - 1
result.append(named((addr, last_addr, size, num_pages, page_size),
"addr last_addr size num_pages page_size"))
addr += size
return result
def list_dfu_devices(*args, **kwargs):
"""Prints a lits of devices detected in DFU mode."""
devices = get_dfu_devices(*args, **kwargs)
if not devices:
print("No DFU capable devices found")
return
for device in devices:
print("Bus {} Device {:03d}: ID {:04x}:{:04x}"
.format(device.bus, device.address,
device.idVendor, device.idProduct))
layout = get_memory_layout(device)
print("Memory Layout")
for entry in layout:
print(" 0x{:x} {:2d} pages of {:3d}K bytes"
.format(entry['addr'], entry['num_pages'],
entry['page_size'] // 1024))
def write_elements(elements, mass_erase_used, progress=None):
"""Writes the indicated elements into the target memory,
erasing as needed.
"""
mem_layout = get_memory_layout(__dev)
for elem in elements:
addr = elem['addr']
size = elem['size']
data = elem['data']
elem_size = size
elem_addr = addr
if progress:
progress(elem_addr, 0, elem_size)
while size > 0:
write_size = size
if not mass_erase_used:
for segment in mem_layout:
if addr >= segment['addr'] and \
addr <= segment['last_addr']:
# We found the page containing the address we want to
# write, erase it
page_size = segment['page_size']
page_addr = addr & ~(page_size - 1)
if addr + write_size > page_addr + page_size:
write_size = page_addr + page_size - addr
page_erase(page_addr)
break
write_memory(addr, data[:write_size], progress,
elem_addr, elem_size)
data = data[write_size:]
addr += write_size
size -= write_size
if progress:
progress(elem_addr, addr - elem_addr, elem_size)
def cli_progress(addr, offset, size):
"""Prints a progress report suitable for use on the command line."""
width = 25
done = offset * width // size
print("\r0x{:08x} {:7d} [{}{}] {:3d}% "
.format(addr, size, '=' * done, ' ' * (width - done),
offset * 100 // size), end="")
sys.stdout.flush()
if offset == size:
print("")
def main():
"""Test program for verifying this files functionality."""
global __verbose
# Parse CMD args
parser = argparse.ArgumentParser(description='DFU Python Util')
#parser.add_argument("path", help="file path")
parser.add_argument(
"-l", "--list",
help="list available DFU devices",
action="store_true",
default=False
)
parser.add_argument(
"-m", "--mass-erase",
help="mass erase device",
action="store_true",
default=False
)
parser.add_argument(
"-u", "--upload",
help="read file from DFU device",
dest="path",
default=False
)
parser.add_argument(
"-v", "--verbose",
help="increase output verbosity",
action="store_true",
default=False
)
args = parser.parse_args()
__verbose = args.verbose
if args.list:
list_dfu_devices(idVendor=__VID, idProduct=__PID)
return
init()
if args.mass_erase:
print ("Mass erase...")
mass_erase()
if args.path:
elements = read_dfu_file(args.path)
if not elements:
return
print("Writing memory...")
write_elements(elements, args.mass_erase, progress=cli_progress)
print("Exiting DFU...")
exit_dfu()
return
print("No command specified")
if __name__ == '__main__':
main()

View File

@ -1,49 +0,0 @@
from pydfu import *
import urllib.request, tempfile, os, shutil, ssl
def firmware_update(verbose):
global __verbose
__verbose = verbose
temp_path = tempfile.mktemp("firmware.dfu")
url = "https://update.badge.emfcamp.org/firmware.dfu"
print("Hello - Welcome to the automated TiLDA Mk4 firmware updater")
print("Finding badge: ", end="")
try:
init()
print("DONE")
print("Downloading newest firmware: ", end="")
context = ssl._create_unverified_context()
with urllib.request.urlopen(url, context=context) as response:
with open(temp_path, 'wb') as tmp_file:
shutil.copyfileobj(response, tmp_file)
print("DONE")
elements = read_dfu_file(temp_path)
if not elements:
return
print("Resetting Badge: ", end="")
mass_erase()
print("DONE")
print("Updating...")
write_elements(elements, True, progress=cli_progress)
exit_dfu()
print("")
print("You can now restart your badge by pressing the reset button on the back. Please follow the instructions on the screen to finish the setup")
print("Have a nice day!")
except ValueError as e:
print("FAIL")
print("")
print("We couldn't find your badge. You need to make sure it's plugged in and in DFU mode.")
print("To put your badge into DFU mode you need to press the joystick in the middle while pressing the reset button at the back.")
print("After that, please try this script again.")
print()
print("Error: %s" %(e))
finally:
if os.path.isfile(temp_path): os.remove(temp_path)

View File

@ -87,9 +87,10 @@ def get_resources(path):
if upip_lib.startswith(".") or upip_lib == "__pycache__":
continue
full_lib_path = os.path.join(full_path, upip_lib)
rel_lib_path = os.path.join(sub_path, upip_lib)
files = {}
if os.path.isfile(full_lib_path):
files = {full_lib_path: None}
files = {rel_lib_path: None}
upip_lib = upip_lib.rsplit('.', 1)[0]
else:
for rel_path in _scan_files(full_lib_path, os.path.join(sub_path, upip_lib)):
@ -134,13 +135,13 @@ def add_metadata(path, resources):
for resource in resources.values():
file = None
if resource['type'] == "app":
file = next(f for f in resource['files'] if "/main.py" in f)
file = next(f for f in resource['files'] if os.path.basename(f) == "main.py")
elif resource['type'] == "lib":
file = next(iter(resource['files'].keys()))
if file:
try:
with open(os.path.join(path, file), "r") as stream:
with open(os.path.join(path, file), "r", encoding='utf8') as stream:
resource.update(_normalize_metadata(read_metadata(stream)))
except ParseException as e:
resource.setdefault("errors", []).append(file + ": " + str(e))
@ -165,6 +166,7 @@ def resolve_dependencies(resources):
to_add = resource['dependencies'].copy()
while len(to_add):
r = to_add.pop()
r = os.path.normpath(r)
if r in already_added:
continue
if r not in resources:
@ -195,7 +197,7 @@ def _validate_resource(path, resource):
if file.endswith(".py"):
try:
filename = os.path.join(path, file)
with open(filename, 'r') as s:
with open(filename, 'r', encoding='utf8') as s:
compile(s.read() + '\n', filename, 'exec')
except Exception as e:
resource.setdefault("errors", []).append(str(e))
@ -243,6 +245,6 @@ def pretty_print_resources(resources):
def normalize_dependency(dependency):
"""lib dependencies can be shortened to just their module name"""
if "." in dependency or "/" in dependency or "upip:" in dependency:
if "." in dependency or os.pathsep in dependency or "upip:" in dependency:
return dependency
return "lib/%s.py" % dependency
return os.path.join("lib", "%s.py" % dependency)

View File

@ -1,7 +1,7 @@
import os, shutil, sys, fnmatch
import os, shutil, sys, fnmatch, glob, pyboard_util
def sync(storage, patterns, resources, verbose):
root = get_root()
def sync(args, patterns, resources, verbose, skip_wifi):
root = get_root(verbose)
# Add all paths that are already files
paths = set([p for p in (patterns or []) if os.path.isfile(os.path.join(root, p))])
@ -10,9 +10,10 @@ def sync(storage, patterns, resources, verbose):
paths.add("boot.py")
# wifi.json
wifi_path = os.path.join(root, "wifi.json")
if os.path.isfile(wifi_path):
paths.add(wifi_path)
if not skip_wifi:
wifi_path = os.path.join(root, "wifi.json")
if os.path.isfile(wifi_path):
paths.add(wifi_path)
if not patterns:
patterns = ["*"]
@ -28,30 +29,27 @@ def sync(storage, patterns, resources, verbose):
print("Resource %s is going to be synced" % key)
for path in resource['files'].keys():
paths.add(path)
if not found:
if not found and (pattern not in paths):
print("WARN: No resources to copy found for pattern %s" % patterns)
pyboard_util.init_copy_via_repl(args)
if not verbose:
print("Copying %s files: " % len(paths), end="")
print("Copying %s files: " % len(paths), end="", flush=True)
for path in paths:
if not path:
continue
rel_path = os.path.relpath(path, root)
if rel_path.startswith(".") or os.path.isdir(path) or os.path.islink(path):
continue
if verbose:
print("Copying %s..." % rel_path)
else:
print(".", end="")
target = os.path.join(storage, rel_path)
target_dir = os.path.dirname(target)
if os.path.isfile(target_dir):
# micropython has the tendency to sometimes corrupt directories into files
os.remove(target_dir)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
shutil.copy2(path, target)
updated = pyboard_util.copy_via_repl(args, path, rel_path)
if verbose:
print("Copied %s, updated: %s" % (rel_path, updated))
else:
if updated:
print("+", end="", flush=True)
else:
print("=", end="", flush=True)
pyboard_util.end_copy_via_repl(args)
if verbose:
print("Files copied successfully")
@ -59,18 +57,19 @@ def sync(storage, patterns, resources, verbose):
print(" DONE")
return synced_resources
def set_boot_app(storage, app_to_boot):
path = os.path.join(storage, 'once.txt')
try:
os.remove(path)
except OSError:
pass
with open(path, 'w') as f:
f.write(app_to_boot + "\n")
def clean(args):
pyboard_util.clean_via_repl(args)
def set_boot_app(args, app_to_boot):
content = app_to_boot + "\n"
pyboard_util.write_via_repl(args, content.encode("utf8"), 'once.txt')
if app_to_boot:
print("setting next boot to %s" % app_to_boot)
def get_root():
def set_no_boot(args):
pyboard_util.write_via_repl(args, b"\n", 'no_boot')
def get_root(verbose=False):
root = os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..'))
if not os.path.isfile(os.path.join(root, "boot.py")):
print("Path %s doesn't contain a boot.py, aborting. Something is probably wrong with your setup.")

View File

@ -11,18 +11,18 @@ $ tilda_tools reset
Soft reboot badge and start specific app
$ tilda_tools reset --boot my_app
Update files on the badge to match the current local version, restarts afterwards
$ tilda_tools sync
Update files in folder(s) to match current local version
$ tilda_tools sync my_game shared
$ tilda_tools sync <pattern1> <pattern2> ...
Sync (as above), but execute my_app after reboot
$ tilda_toold.py sync --boot my_app [<other sync parameter>]
$ tilda_tools sync --boot my_app [<other sync parameter>]
Sync (as above), but execute a single file afterwards without copying it to the badge
$ tilda_toold.py sync --run some_other_file.py
$ tilda_tools sync --run some_other_file.py
Sync a given app and execute it
$ tilda_tools app home_default
Executes a single file on the badge without copying anything (Using pyboard.py)
$ tilda_tools run my_app/main.py
@ -47,18 +47,21 @@ Common parameters
"""
import sys, glob
import sync, pyboard_util, wifi
import glob
import sync, firmware_update, wifi, pyboard_util, sys
from resources import *
def main():
import argparse
cmd_parser = argparse.ArgumentParser(description='Toolchain for working with the TiLDA Mk4')
cmd_parser.add_argument('command', nargs=1, help='command [test|reset|sync|run|validate|wifi|firmware-update]', choices=['test', 'reset', 'sync', 'validate', 'run', 'wifi', 'firmware-update'])
cmd_parser.add_argument('command', nargs=1, help='command [test|reset|sync|run|validate|wifi|firmware-update|app|bootstrap]', choices=['test', 'reset', 'sync', 'validate', 'run', 'wifi', 'firmware-update', 'app', 'bootstrap'])
cmd_parser.add_argument('-c', '--clean', action='store_true', help='clean mass storage before writing')
cmd_parser.add_argument('-d', '--device', help='the serial device of the badge')
cmd_parser.add_argument('-s', '--storage', help='the usb mass storage path of the badge')
cmd_parser.add_argument('-b', '--baudrate', default=115200, help='the baud rate of the serial device')
cmd_parser.add_argument('-v', '--verbose', action='store_true', help='adds more output')
cmd_parser.add_argument('--skip-wifi', action='store_true', help='does not sync wifi.json')
cmd_parser.add_argument('--bootstrapped-apps', action='store_true', help='[Sync] only bootstrapped apps by default')
cmd_parser.add_argument('--print_resources', action='store_true', help='prints resources in json')
cmd_parser.add_argument('--boot', help='defines which app to boot into after reboot')
cmd_parser.add_argument('--run', help='like run, but after a sync')
@ -69,14 +72,25 @@ def main():
path = sync.get_root()
run_tests = command == "test"
if command not in ["validate"]:
try:
import serial
except Exception as e:
print("Please install pyserial first: https://pyserial.readthedocs.io/en/latest/pyserial.html")
sys.exit(1)
if command == "firmware-update":
import pydfu_util # to avoid having a "usb" dependency for other calls
pydfu_util.firmware_update(args.verbose)
firmware_update.firmware_update(args.verbose)
if command == "wifi":
wifi.select_wifi()
if command in ["test", "validate", "sync"]:
if command == "app":
command = "sync"
args.run = "%s/main.py" % args.paths[0]
#args.boot = args.paths[0]
if command in ["test", "validate", "sync", "bootstrap"]:
resources = get_resources(path)
add_metadata(path, resources)
validate(path, resources)
@ -93,24 +107,44 @@ def main():
if command == "test":
command = "sync"
if len(args.paths) == 0:
args.paths = ["lib/test_*"]
print("Please define an app or lib to sync: tilda_tools sync my_app\n")
sys.exit(1)
else:
args.paths = ["lib/test_%s.py" % p for p in args.paths]
if command in ["reset", "sync"]:
if command in ["reset", "sync", "bootstrap"]:
pyboard_util.stop_badge(args, args.verbose)
if command == "bootstrap":
sync.clean(args)
sync.sync(args, ["bootstrap.py"], {}, args.verbose, args.skip_wifi)
pyboard_util.hard_reset(args)
if command == "sync":
paths = args.paths if len(args.paths) else None
synced_resources = sync.sync(get_storage(args), paths, resources, args.verbose)
if args.bootstrapped_apps:
for k,val in list(resources.items()):
requested = paths and k in paths
bootstrapped = val.get("bootstrapped", False)
if val.get("type", None) == "app":
if not (bootstrapped or (paths and requested)):
# App is not in the bootstrap list, and isn't explicitly requested
if args.verbose:
print("Removing app '{0}' from sync list".format(k))
del resources[k]
if command in ["reset", "sync"]:
sync.set_boot_app(get_storage(args), args.boot or "")
pyboard_util.soft_reset(args)
if args.clean:
sync.clean(args)
synced_resources = sync.sync(args, paths, resources, args.verbose, args.skip_wifi)
if (command in ["reset", "sync"]) or run_tests:
sync.set_boot_app(args, args.boot or "")
if args.run:
command = "run"
args.paths = [args.run]
sync.set_no_boot(args)
pyboard_util.soft_reset(args)
if command == "run":
pyboard_util.check_run(args.paths)
@ -122,24 +156,8 @@ def main():
pyboard_util.run(args, [resource], False)
pyboard_util.soft_reset(args, False)
pyboard_util.close_pyb()
def find_storage():
# todo: find solution for windows and linux
for pattern in ['/Volumes/PYBFLASH', '/Volumes/NO NAME']:
for path in glob.glob(pattern):
return path
print("Couldn't find badge storage - Please make it's plugged in and reset it if necessary")
sys.exit(1)
def get_storage(args):
if not args.storage:
args.storage = find_storage()
return args.storage
if __name__ == "__main__":
main()

5
.gitignore vendored
View File

@ -1,3 +1,6 @@
.DS_Store
__pycache__
wifi.json
wifi*.json
config.json
cmd.exe.lnk
tilda_tools.bat

50
3dspin/coriolis.obj Normal file
View File

@ -0,0 +1,50 @@
# Exported from Wings 3D 1.5.4
mtllib coriolis.mtl
o Cube1
#12 vertices, 14 faces
v -1.00000000 -1.00000000 0.0000000e+0
v -1.00000000 0.0000000e+0 -1.00000000
v 0.0000000e+0 -1.00000000 -1.00000000
v -1.00000000 0.0000000e+0 1.00000000
v 0.0000000e+0 -1.00000000 1.00000000
v 0.0000000e+0 1.00000000 -1.00000000
v -1.00000000 1.00000000 0.0000000e+0
v 0.0000000e+0 1.00000000 1.00000000
v 1.00000000 -1.00000000 0.0000000e+0
v 1.00000000 0.0000000e+0 -1.00000000
v 1.00000000 0.0000000e+0 1.00000000
v 1.00000000 1.00000000 0.0000000e+0
v 0.2 0.1 1.01
v 0.2 -0.1 1.01
v -0.2 -0.1 1.01
v -0.2 0.1 1.01
vn -0.70710678 -0.70710678 0.0000000e+0
vn -0.70710678 0.0000000e+0 -0.70710678
vn 0.0000000e+0 -0.70710678 -0.70710678
vn -0.70710678 0.0000000e+0 0.70710678
vn 0.0000000e+0 -0.70710678 0.70710678
vn 0.0000000e+0 0.70710678 -0.70710678
vn -0.70710678 0.70710678 0.0000000e+0
vn 0.0000000e+0 0.70710678 0.70710678
vn 0.70710678 -0.70710678 0.0000000e+0
vn 0.70710678 0.0000000e+0 -0.70710678
vn 0.70710678 0.0000000e+0 0.70710678
vn 0.70710678 0.70710678 0.0000000e+0
g Cube1_default
usemtl default
s 1
f 1//1 2//2 3//3
f 1//1 4//4 7//7 2//2
f 1//1 5//5 4//4
f 2//2 6//6 10//10 3//3
f 2//2 7//7 6//6
f 3//3 9//9 5//5 1//1
f 3//3 10//10 9//9
f 4//4 8//8 7//7
f 5//5 9//9 11//11
f 5//5 11//11 8//8 4//4
f 6//6 12//12 10//10
f 7//7 8//8 12//12 6//6
f 8//8 11//11 12//12
f 10//10 12//12 11//11 9//9
f 16 15 14 13

29
3dspin/cube.obj Normal file
View File

@ -0,0 +1,29 @@
# Exported from Wings 3D 1.5.4
mtllib cube.mtl
o Cube1
#8 vertices, 6 faces
v -1.00000000 -1.00000000 -1.00000000
v -1.00000000 -1.00000000 1.00000000
v -1.00000000 1.00000000 -1.00000000
v -1.00000000 1.00000000 1.00000000
v 1.00000000 -1.00000000 -1.00000000
v 1.00000000 -1.00000000 1.00000000
v 1.00000000 1.00000000 -1.00000000
v 1.00000000 1.00000000 1.00000000
vn -0.57735027 -0.57735027 -0.57735027
vn -0.57735027 -0.57735027 0.57735027
vn -0.57735027 0.57735027 -0.57735027
vn -0.57735027 0.57735027 0.57735027
vn 0.57735027 -0.57735027 -0.57735027
vn 0.57735027 -0.57735027 0.57735027
vn 0.57735027 0.57735027 -0.57735027
vn 0.57735027 0.57735027 0.57735027
g Cube1_default
usemtl default
s 1
f 1//1 5//5 6//6 2//2
f 2//2 4//4 3//3 1//1
f 2//2 6//6 8//8 4//4
f 3//3 7//7 5//5 1//1
f 4//4 8//8 7//7 3//3
f 5//5 7//7 8//8 6//6

59
3dspin/dodecahedron.obj Normal file
View File

@ -0,0 +1,59 @@
# Exported from Wings 3D 1.5.4
mtllib dodecahedron.mtl
o dodecahedron1
#20 vertices, 12 faces
v -0.50000000 0.0000000e+0 1.30901699
v 0.50000000 0.0000000e+0 1.30901699
v -0.80901699 -0.80901699 -0.80901699
v -0.80901699 -0.80901699 0.80901699
v -0.80901699 0.80901699 -0.80901699
v -0.80901699 0.80901699 0.80901699
v 0.80901699 -0.80901699 -0.80901699
v 0.80901699 -0.80901699 0.80901699
v 0.80901699 0.80901699 -0.80901699
v 0.80901699 0.80901699 0.80901699
v 1.30901699 0.50000000 0.0000000e+0
v 1.30901699 -0.50000000 0.0000000e+0
v -1.30901699 0.50000000 0.0000000e+0
v -1.30901699 -0.50000000 0.0000000e+0
v -0.50000000 0.0000000e+0 -1.30901699
v 0.50000000 0.0000000e+0 -1.30901699
v 0.0000000e+0 1.30901699 0.50000000
v 0.0000000e+0 1.30901699 -0.50000000
v 0.0000000e+0 -1.30901699 0.50000000
v 0.0000000e+0 -1.30901699 -0.50000000
vn -0.35682209 0.0000000e+0 0.93417236
vn 0.35682209 0.0000000e+0 0.93417236
vn -0.57735027 -0.57735027 -0.57735027
vn -0.57735027 -0.57735027 0.57735027
vn -0.57735027 0.57735027 -0.57735027
vn -0.57735027 0.57735027 0.57735027
vn 0.57735027 -0.57735027 -0.57735027
vn 0.57735027 -0.57735027 0.57735027
vn 0.57735027 0.57735027 -0.57735027
vn 0.57735027 0.57735027 0.57735027
vn 0.93417236 0.35682209 0.0000000e+0
vn 0.93417236 -0.35682209 0.0000000e+0
vn -0.93417236 0.35682209 0.0000000e+0
vn -0.93417236 -0.35682209 0.0000000e+0
vn -0.35682209 0.0000000e+0 -0.93417236
vn 0.35682209 0.0000000e+0 -0.93417236
vn 0.0000000e+0 0.93417236 0.35682209
vn 0.0000000e+0 0.93417236 -0.35682209
vn 0.0000000e+0 -0.93417236 0.35682209
vn 0.0000000e+0 -0.93417236 -0.35682209
g dodecahedron1_default
usemtl default
s 1
f 1//1 4//4 19//19 8//8 2//2
f 1//1 6//6 13//13 14//14 4//4
f 2//2 10//10 17//17 6//6 1//1
f 3//3 20//20 19//19 4//4 14//14
f 5//5 18//18 9//9 16//16 15//15
f 7//7 16//16 9//9 11//11 12//12
f 8//8 12//12 11//11 10//10 2//2
f 9//9 18//18 17//17 10//10 11//11
f 12//12 8//8 19//19 20//20 7//7
f 13//13 6//6 17//17 18//18 5//5
f 14//14 13//13 5//5 15//15 3//3
f 15//15 16//16 7//7 20//20 3//3

51
3dspin/icosahedron.obj Executable file
View File

@ -0,0 +1,51 @@
# Exported from Wings 3D 1.5.4
mtllib icosahedron.mtl
o icosahedron1
#12 vertices, 20 faces
v 0.0000000e+0 1.90211303 0.0000000e+0
v 1.70130162 0.85065081 0.0000000e+0
v 0.52573111 0.85065081 1.61803399
v -1.37638192 0.85065081 1.00000000
v -1.37638192 0.85065081 -1.00000000
v 0.52573111 0.85065081 -1.61803399
v -1.70130162 -0.85065081 0.0000000e+0
v -0.52573111 -0.85065081 -1.61803399
v 1.37638192 -0.85065081 -1.00000000
v 1.37638192 -0.85065081 1.00000000
v -0.52573111 -0.85065081 1.61803399
v 0.0000000e+0 -1.90211303 0.0000000e+0
vn 2.7942283e-17 1.00000000 -1.3971142e-17
vn 0.89442719 0.44721360 0.0000000e+0
vn 0.27639320 0.44721360 0.85065081
vn -0.72360680 0.44721360 0.52573111
vn -0.72360680 0.44721360 -0.52573111
vn 0.27639320 0.44721360 -0.85065081
vn -0.89442719 -0.44721360 0.0000000e+0
vn -0.27639320 -0.44721360 -0.85065081
vn 0.72360680 -0.44721360 -0.52573111
vn 0.72360680 -0.44721360 0.52573111
vn -0.27639320 -0.44721360 0.85065081
vn -4.1913425e-17 -1.00000000 0.0000000e+0
g icosahedron1_default
usemtl default
s 1
f 1//1 3//3 2//2
f 1//1 4//4 3//3
f 1//1 5//5 4//4
f 1//1 6//6 5//5
f 2//2 6//6 1//1
f 2//2 9//9 6//6
f 2//2 10//10 9//9
f 3//3 10//10 2//2
f 3//3 11//11 10//10
f 4//4 11//11 3//3
f 5//5 7//7 4//4
f 5//5 8//8 7//7
f 6//6 8//8 5//5
f 6//6 9//9 8//8
f 7//7 11//11 4//4
f 7//7 12//12 11//11
f 8//8 12//12 7//7
f 9//9 12//12 8//8
f 10//10 12//12 9//9
f 11//11 12//12 10//10

361
3dspin/main.py Normal file
View File

@ -0,0 +1,361 @@
"""3d rotating polyhedra. 2016 badge competition winner, ported for 2018!"""
___title___ = "3D Spin"
___license___ = "MIT"
___categories___ = ["Demo"]
___dependencies___ = ["app", "ugfx_helper", "sleep", "buttons"]
import ugfx
from tilda import Buttons
import math
from uos import listdir
import time
# from imu import IMU
import gc
# import pyb
import app
app_path = './3dspin'
from math import sqrt
class Vector3D:
def __init__(self, x=0.0, y=0.0, z=0.0):
self.x = x
self.y = y
self.z = z
def magnitude(self):
return sqrt(self.x*self.x+self.y*self.y+self.z*self.z)
def __sub__(self, v):
return Vector3D(self.x-v.x, self.y-v.y, self.z-v.z)
def normalize(self):
mag = self.magnitude()
if (mag > 0.0):
self.x /= mag
self.y /= mag
self.z /= mag
else:
raise Exception('*** Vector: error, normalizing zero vector! ***')
def cross(self, v): #cross product
return Vector3D(self.y*v.z-self.z*v.y, self.z*v.x-self.x*v.z, self.x*v.y-self.y*v.x)
#The layout of the matrix (row- or column-major) matters only when the user reads from or writes to the matrix (indexing). For example in the multiplication function we know that the first components of the Matrix-vectors need to be multiplied by the vector. The memory-layout is not important
class Matrix:
''' Column-major order '''
def __init__(self, createidentity=True):# (2,2) creates a 2*2 Matrix
# if rows < 2 or cols < 2:
# raise Exception('*** Matrix: error, getitem((row, col)), row, col problem! ***')
self.rows = 4
self.cols = 4
self.m = [[0.0]*self.rows for x in range(self.cols)]
#If quadratic matrix then create identity one
if createidentity:
for i in range(self.rows):
self.m[i][i] = 1.0
def mul(self, right):
if isinstance(right, Matrix):
r = Matrix(False)
for i in range(self.rows):
for j in range(right.cols):
for k in range(self.cols):
r.m[i][j] += self.m[i][k]*right.m[k][j]
return r
elif isinstance(right, Vector3D): #Translation: the last column of the matrix. Remains unchanged due to the the fourth coord of the vector (1).
# if self.cols == 4:
r = Vector3D()
addx = addy = addz = 0.0
if self.rows == self.cols == 4:
addx = self.m[0][3]
addy = self.m[1][3]
addz = self.m[2][3]
r.x = self.m[0][0]*right.x+self.m[0][1]*right.y+self.m[0][2]*right.z+addx
r.y = self.m[1][0]*right.x+self.m[1][1]*right.y+self.m[1][2]*right.z+addy
r.z = self.m[2][0]*right.x+self.m[2][1]*right.y+self.m[2][2]*right.z+addz
#In 3D game programming we use homogenous coordinates instead of cartesian ones in case of Vectors in order to be able to use them with a 4*4 Matrix. The 4th coord (w) is not included in the Vector-class but gets computed on the fly
w = self.m[3][0]*right.x+self.m[3][1]*right.y+self.m[3][2]*right.z+self.m[3][3]
if (w != 1 and w != 0):
r.x = r.x/w;
r.y = r.y/w;
r.z = r.z/w;
return r
else:
raise Exception('*** Matrix: error, matrix multiply with not matrix, vector or int or float! ***')
def loadObject(filename):
print(filename)
if (".obj" in filename):
loadObj(filename)
if (".dat" in filename):
loadDat(filename)
def loadDat(filename):
global obj_vertices
global obj_faces
obj_vertices = []
obj_faces = []
f = open(app_path + "/" + filename)
for line in f:
if line[:2] == "v ":
parts = line.split(" ")
obj_vertices.append(
Vector3D(
float(parts[1]),
float(parts[2]),
float(parts[3])
)
)
gc.collect()
elif line[:2] == "f ":
parts = line.split(" ")
face = []
for part in parts[1:]:
face.append(int(part.split("/",1)[0])-1)
obj_faces.append(face)
gc.collect()
f.close()
def loadObj(filename):
global obj_vertices
global obj_faces
obj_vertices = []
obj_faces = []
f = open(app_path + "/" + filename)
for line in f:
if line[:2] == "v ":
parts = line.split(" ")
obj_vertices.append(
Vector3D(
float(parts[1]),
float(parts[2]),
float(parts[3])
)
)
gc.collect()
elif line[:2] == "f ":
parts = line.split(" ")
face = []
for part in parts[1:]:
face.append(int(part.split("/",1)[0])-1)
obj_faces.append(face)
gc.collect()
f.close()
def toScreenCoords(pv):
px = int((pv.x+1)*0.5*240)
py = int((1-(pv.y+1)*0.5)*320)
return [px, py]
def createCameraMatrix(x,y,z):
camera_transform = Matrix()
camera_transform.m[0][3] = x
camera_transform.m[1][3] = y
camera_transform.m[2][3] = z
return camera_transform
def createProjectionMatrix(horizontal_fov, zfar, znear):
s = 1/(math.tan(math.radians(horizontal_fov/2)))
proj = Matrix()
proj.m[0][0] = s * (320/240) # inverse aspect ratio
proj.m[1][1] = s
proj.m[2][2] = -zfar/(zfar-znear)
proj.m[3][2] = -1.0
proj.m[2][3] = -(zfar*znear)/(zfar-znear)
return proj
def createRotationMatrix(x_rotation, y_rotation, z_rotation):
rot_x = Matrix()
rot_x.m[1][1] = rot_x.m[2][2] = math.cos(x_rotation)
rot_x.m[2][1] = math.sin(x_rotation)
rot_x.m[1][2] = -rot_x.m[2][1]
rot_y = Matrix()
rot_y.m[0][0] = rot_y.m[2][2] = math.cos(y_rotation)
rot_y.m[0][2] = math.sin(y_rotation)
rot_y.m[2][0] = -rot_y.m[0][2]
rot_z = Matrix()
rot_z.m[0][0] = rot_z.m[1][1] = math.cos(z_rotation)
rot_z.m[1][0] = math.sin(z_rotation)
rot_z.m[0][1] = -rot_z.m[1][0]
return rot_z.mul(rot_x).mul(rot_y)
def normal(face, vertices, normalize = True):
# Work out the face normal for lighting
normal = (vertices[face[1]]-vertices[face[0]]).cross(vertices[face[2]]-vertices[face[0]])
if normalize == True:
normal.normalize()
return normal
def clear_screen():
# Selectively clear the screen by re-rendering the previous frame in black
global last_polygons
global last_mode
for poly in last_polygons:
if last_mode == FLAT:
ugfx.fill_polygon(0,0, poly, ugfx.BLACK)
ugfx.polygon(0,0, poly, ugfx.BLACK)
def render(mode, rotation):
# Rotate all the vertices in one go
vertices = [rotation.mul(vertex) for vertex in obj_vertices]
# Calculate normal for each face (for lighting)
if mode == FLAT:
face_normal_zs = [normal(face, vertices).z for face in obj_faces]
# Project (with camera) all the vertices in one go as well
vertices = [camera_projection.mul(vertex) for vertex in vertices]
# Calculate projected normals for each face
if mode != WIREFRAME:
proj_normal_zs = [normal(face, vertices, False).z for face in obj_faces]
# Convert to screen coordinates all at once
# We could do this faster by only converting vertices that are
# in faces that will be need rendered, but it's likely that test
# would take longer.
vertices = [toScreenCoords(v) for v in vertices]
# Render the faces to the screen
vsync()
clear_screen()
global last_polygons
global last_mode
last_polygons = []
last_mode = mode
for index in range(len(obj_faces)):
# Only render things facing towards us (unless we're in wireframe mode)
if (mode == WIREFRAME) or (proj_normal_zs[index] > 0):
# Convert polygon
poly = [vertices[v] for v in obj_faces[index]]
# Calculate colour and render
ugcol = ugfx.WHITE
if mode == FLAT:
# Simple lighting calculation
colour5 = int(face_normal_zs[index] * 31)
colour6 = int(face_normal_zs[index] * 63)
# Create a 5-6-5 grey
ugcol = (colour5 << 11) | (colour6 << 5) | colour5
# Render polygon
ugfx.fill_polygon(0,0, poly, ugcol)
# Always draw the wireframe in the same colour to fill gaps left by the
# fill_polygon method
ugfx.polygon(0,0, poly, ugcol)
last_polygons.append(poly)
def vsync():
None
# while(tear.value() == 0):
# pass
# while(tear.value()):
# pass
def calculateRotation(smoothing, accelerometer):
# Keep a list of recent rotations to smooth things out
global x_rotation
global z_rotation
# First, pop off the oldest rotation
# if len(x_rotations) >= smoothing:
# x_rotations = x_rotations[1:]
# if len(z_rotations) >= smoothing:
# z_rotations = z_rotations[1:]
# Now append a new rotation
pi_2 = math.pi / 2
#x_rotations.append(-accelerometer['z'] * pi_2)
#z_rotations.append(accelerometer['x'] * pi_2)
# Calculate rotation matrix
return createRotationMatrix(
# this averaging isn't correct in the first <smoothing> frames, but who cares
math.radians(x_rotation),
math.radians(y_rotation),
math.radians(z_rotation)
)
print("Hello 3DSpin")
# Initialise hardware
ugfx.init()
ugfx.clear(ugfx.BLACK)
# imu=IMU()
# buttons.init()
# Enable tear detection for vsync
# ugfx.enable_tear()
# tear = pyb.Pin("TEAR", pyb.Pin.IN)
#ugfx.set_tear_line(1)
print("Graphics initalised")
# Set up static rendering matrices
camera_transform = createCameraMatrix(0, 0, -5.0)
proj = createProjectionMatrix(45.0, 100.0, 0.1)
camera_projection = proj.mul(camera_transform)
print("Camera initalised")
# Get the list of available objects, and load the first one
obj_vertices = []
obj_faces = []
print("available objects: {}", listdir(app_path))
objects = [x for x in listdir(app_path) if (((".obj" in x) | (".dat" in x)) & (x[0] != "."))]
selected = 0
loadObject(objects[selected])
print("loaded object {}", objects[selected])
# Set up rotation tracking arrays
x_rotation = 0
z_rotation = 0
y_rotation = 0
# Smooth rotations over 5 frames
smoothing = 5
# Rendering modes
BACKFACECULL = 1
FLAT = 2
WIREFRAME = 3
# Start with backface culling mode
mode = BACKFACECULL
last_polygons = []
last_mode = WIREFRAME
# Main loop
run = True
while run:
gc.collect()
# Render the scene
render(
mode,
calculateRotation(smoothing, None)
)
# Button presses
y_rotation += 5
x_rotation += 3
z_rotation += 1
if Buttons.is_pressed(Buttons.JOY_Left):
y_rotation -= 5
if Buttons.is_pressed(Buttons.JOY_Right):
y_rotation += 5
if Buttons.is_pressed(Buttons.JOY_Center):
y_rotation = 0
if Buttons.is_pressed(Buttons.BTN_B):
selected += 1
if selected >= len(objects):
selected = 0
loadObject(objects[selected])
time.sleep_ms(500) # Wait a while to avoid skipping ahead if the user still has the button down
if Buttons.is_pressed(Buttons.BTN_A):
mode += 1
if mode > 3:
mode = 1
time.sleep_ms(500) # Wait a while to avoid skipping ahead if the user still has the button down
if Buttons.is_pressed(Buttons.BTN_Menu):
run = False
app.restart_to_default()

27
3dspin/octohedron.obj Normal file
View File

@ -0,0 +1,27 @@
# Exported from Wings 3D 2.0.5
mtllib octohedron.mtl
o octahedron1
#6 vertices, 8 faces
v 2.00000000 0.0000000e+0 0.0000000e+0
v -2.00000000 0.0000000e+0 0.0000000e+0
v 0.0000000e+0 2.00000000 0.0000000e+0
v 0.0000000e+0 -2.00000000 0.0000000e+0
v 0.0000000e+0 0.0000000e+0 2.00000000
v 0.0000000e+0 0.0000000e+0 -2.00000000
vn 1.00000000 0.0000000e+0 0.0000000e+0
vn -1.00000000 0.0000000e+0 0.0000000e+0
vn 0.0000000e+0 1.00000000 0.0000000e+0
vn 0.0000000e+0 -1.00000000 0.0000000e+0
vn 0.0000000e+0 0.0000000e+0 1.00000000
vn 0.0000000e+0 0.0000000e+0 -1.00000000
g octahedron1_default
usemtl default
s 1
f 1//1 5//5 4//4
f 1//1 6//6 3//3
f 2//2 5//5 3//3
f 2//2 6//6 4//4
f 3//3 5//5 1//1
f 3//3 6//6 2//2
f 4//4 5//5 2//2
f 4//4 6//6 1//1

81
3dspin/octotoad.obj Normal file
View File

@ -0,0 +1,81 @@
# Exported from Wings 3D 2.0.5
mtllib octoad.mtl
o octotoad1
#24 vertices, 26 faces
v 1.66800000 0.55600000 0.55600000
v 1.66800000 0.55600000 -0.55600000
v 1.66800000 -0.55600000 0.55600000
v 1.66800000 -0.55600000 -0.55600000
v -1.66800000 0.55600000 0.55600000
v -1.66800000 0.55600000 -0.55600000
v -1.66800000 -0.55600000 0.55600000
v -1.66800000 -0.55600000 -0.55600000
v 0.55600000 1.66800000 0.55600000
v 0.55600000 1.66800000 -0.55600000
v 0.55600000 -1.66800000 0.55600000
v 0.55600000 -1.66800000 -0.55600000
v 0.55600000 0.55600000 1.66800000
v 0.55600000 0.55600000 -1.66800000
v 0.55600000 -0.55600000 1.66800000
v 0.55600000 -0.55600000 -1.66800000
v -0.55600000 1.66800000 0.55600000
v -0.55600000 1.66800000 -0.55600000
v -0.55600000 -1.66800000 0.55600000
v -0.55600000 -1.66800000 -0.55600000
v -0.55600000 0.55600000 1.66800000
v -0.55600000 0.55600000 -1.66800000
v -0.55600000 -0.55600000 1.66800000
v -0.55600000 -0.55600000 -1.66800000
vn 0.85476344 0.36700100 0.36700100
vn 0.85476344 0.36700100 -0.36700100
vn 0.85476344 -0.36700100 0.36700100
vn 0.85476344 -0.36700100 -0.36700100
vn -0.85476344 0.36700100 0.36700100
vn -0.85476344 0.36700100 -0.36700100
vn -0.85476344 -0.36700100 0.36700100
vn -0.85476344 -0.36700100 -0.36700100
vn 0.36700100 0.85476344 0.36700100
vn 0.36700100 0.85476344 -0.36700100
vn 0.36700100 -0.85476344 0.36700100
vn 0.36700100 -0.85476344 -0.36700100
vn 0.36700100 0.36700100 0.85476344
vn 0.36700100 0.36700100 -0.85476344
vn 0.36700100 -0.36700100 0.85476344
vn 0.36700100 -0.36700100 -0.85476344
vn -0.36700100 0.85476344 0.36700100
vn -0.36700100 0.85476344 -0.36700100
vn -0.36700100 -0.85476344 0.36700100
vn -0.36700100 -0.85476344 -0.36700100
vn -0.36700100 0.36700100 0.85476344
vn -0.36700100 0.36700100 -0.85476344
vn -0.36700100 -0.36700100 0.85476344
vn -0.36700100 -0.36700100 -0.85476344
g octotoad1_default
usemtl default
s 1
f 1//1 3//3 4//4 2//2
f 1//1 13//13 15//15 3//3
f 2//2 10//10 9//9 1//1
f 2//2 14//14 10//10
f 3//3 11//11 12//12 4//4
f 3//3 15//15 11//11
f 4//4 16//16 14//14 2//2
f 5//5 17//17 18//18 6//6
f 5//5 21//21 17//17
f 6//6 8//8 7//7 5//5
f 6//6 22//22 24//24 8//8
f 7//7 23//23 21//21 5//5
f 8//8 20//20 19//19 7//7
f 8//8 24//24 20//20
f 9//9 13//13 1//1
f 9//9 17//17 21//21 13//13
f 10//10 18//18 17//17 9//9
f 11//11 19//19 20//20 12//12
f 12//12 16//16 4//4
f 12//12 20//20 24//24 16//16
f 13//13 21//21 23//23 15//15
f 14//14 22//22 18//18 10//10
f 15//15 23//23 19//19 11//11
f 16//16 24//24 22//22 14//14
f 18//18 22//22 6//6
f 19//19 23//23 7//7

19
3dspin/tetrahedron.obj Normal file
View File

@ -0,0 +1,19 @@
# Exported from Wings 3D 1.5.4
mtllib tetrahedron.mtl
o tetrahedron1
#4 vertices, 4 faces
v 0.0000000e+0 1.08866211 0.0000000e+0
v 0.0000000e+0 -0.54433105 1.15470054
v -1.00000000 -0.54433105 -0.57735027
v 1.00000000 -0.54433105 -0.57735027
vn 0.0000000e+0 1.00000000 -1.1102230e-16
vn 0.0000000e+0 -0.33333333 0.94280904
vn -0.81649658 -0.33333333 -0.47140452
vn 0.81649658 -0.33333333 -0.47140452
g tetrahedron1_default
usemtl default
s 1
f 1//1 3//3 2//2
f 1//1 4//4 3//3
f 2//2 4//4 1//1
f 3//3 4//4 2//2

67
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,67 @@
# Rules for apps in the official badge store
See [Rules for apps in the official badge store](https://badge.emfcamp.org/wiki/TiLDA_MK4/Badge_Store_Submissions#Rules_for_apps_in_the_official_badge_store)
# Packaging up your badge app
See [Packaging up your badge app for submission to the store](https://badge.emfcamp.org/wiki/TiLDA_MK4/Badge_Store_Submissions#Packaging_up_your_badge_app_for_submission_to_the_store)
# Using git and GitHub to submit your badge app
Please ensure:
1. You have a [GitHub account](https://github.com/join)
2. You have [git installed](https://git-scm.com/downloads) on your local computer
3. You have written a badge app [packaged it up](https://badge.emfcamp.org/wiki/TiLDA_MK4/Badge_Store_Submissions#Packaging_up_your_badge_app_for_submission_to_the_store) and [validated](https://badge.emfcamp.org/wiki/TiLDA_MK4/Badge_Store_Submissions#Packaging_up_your_badge_app_for_submission_to_the_store) it
These instructions are tailored around submitting your Badge App but the general principle can be used to raise any pull request.
Please keep pull requests focused on one thing only (like submission of your app), since this makes it easier to merge and test
in a timely manner.
## Setting up your git username and email address
Using the command line/terminal on your computer type:
```
git config --global user.name “username”
git config --global user.email “username@users.noreply.github.com”
```
Note that the username is the username you use to log into GitHub, not your profile “Name”
## Main flow for contributing
It's important to follow these steps to make changes to your own copy of the emfcamp repo and then raise a pull request that will be merged into the emfcamp repo.
1. Login to GitHub, go to the [emfcamp/Mk4-Apps](https://github.com/emfcamp/Mk4-Apps) repository and click ```Fork``` in
the top right
2. Using the command line/terminal on your computer type: `git clone <url to YOUR fork>`
3. `cd Mk4-Apps`
4. `git checkout master`
5. `git checkout -b my-app-name` to create a branch with your apps name
6. Copy your app files in their uniquely named folder into the Mk4-Apps/ directory (repo root directory)
7. `git add .` to add all of your app files and directory to your local repo
8. `git commit -m "my-app-name badge app"` note that you can put any message in the quotes
9. `git push origin my-app-name` to update *your* GitHub fork with the change
10. Create pull request using the GitHub UI to merge your changes from your new branch into `emfcamp/Mk4-Apps/master`
11. Repeat from step 4 for new other changes.
The primary thing to remember is that separate pull requests should be created for separate branches. Never create a pull request from your `master` branch.
Once you have created the pull request, every new commit/push in your branch will propagate from your
fork into the pull reqests in the main github/emfcamp/Mk4-Apps repo.
## Updating your GitHub and local git repo
Later, you can get the changes from the emfcamp/Mk4-Apps repo into your `master` branch by adding emfcamp as a git remote and
merging from it as follows:
1. `git remote add emfcamp https://github.com/emfcamp/Mk4-Apps.git`
2. `git checkout master`
3. `git fetch emfcamp`
4. `git merge emfcamp/master` will update your local repo
5. `git push origin master` will update your fork on GitHub
## Useful links
The GitHub workflow: https://guides.github.com/introduction/flow/index.html
If you need help with pull requests there are guides on GitHub here: https://help.github.com/articles/creating-a-pull-request/

82
DevRant/main.py Normal file
View File

@ -0,0 +1,82 @@
"""DevRant Client for TiLDA-MK4
"""
___name___ = "DevRant"
___license___ = "MIT"
___dependencies___ = ["app", "wifi", "http", "ugfx_helper"]
___categories___ = ["Other"]
___launchable___ = True
import ugfx, wifi, http, json, utime, ugfx_helper, dialogs, app
char_ln = 25
ln_pg = 19
def loop():
skip = 0
while True:
ugfx.clear(ugfx.html_color(0x544c6d))
data= json.loads(http.get("https://devrant.com/api/devrant/rants?app=3&sort=top&range=day&limit=1&skip="+str(skip)).raise_for_status().content)["rants"][0]
text=data["text"].split(" ")
screens = [[]]
line = ""
screen = 0
for word in text:
if len(line+word)+1 >= char_ln:
if len(screens[screen]) >= ln_pg:
screen+=1
screens.append([])
screens[screen].append(line)
line=word
else:
line = line + " " + word
if len(screens[screen]) < ln_pg:
screens[screen].append(line)
else:
screens.append([line])
hold=True
page = 0
while hold:
ugfx.clear(ugfx.html_color(0x544c6d))
ugfx.area(0,0,240,35,ugfx.html_color(0x41476d))
ugfx.text(5,5,str(data["score"])+"++ " + data["user_username"] + ":",ugfx.BLACK)
ugfx.text(5,20,"Page: " + str(page+1) + "/" + str(len(screens)),ugfx.BLACK)
count = 0
for line in screens[page]:
ugfx.text(5,35+count*15,line,ugfx.BLACK)
count+=1
hold_btn = True
while hold_btn:
if tilda.Buttons.is_pressed(tilda.Buttons.BTN_Menu):
return
if tilda.Buttons.is_pressed(tilda.Buttons.BTN_A):
skip += 1
hold_btn = False
hold = False
while tilda.Buttons.is_pressed(tilda.Buttons.BTN_A):
utime.sleep_ms(10)
if tilda.Buttons.is_pressed(tilda.Buttons.JOY_Right):
if page < len(screens)-1:
page += 1
hold_btn = False
while tilda.Buttons.is_pressed(tilda.Buttons.JOY_Right):
utime.sleep_ms(10)
if tilda.Buttons.is_pressed(tilda.Buttons.JOY_Left):
if page > 0:
page -= 1
hold_btn = False
while tilda.Buttons.is_pressed(tilda.Buttons.JOY_Left):
utime.sleep_ms(10)
ugfx_helper.init()
ugfx.clear()
ugfx.text(5,5, "DevRant for the TiLDA Mk4", ugfx.BLACK)
ugfx.text(5, 40, "Connecting To WIFI", ugfx.BLACK)
wifi.connect()
ugfx.text(5, 40, "Connecting To WIFI", ugfx.WHITE)
loop()
app.restart_to_default()

58
LED_Party/main.py Normal file

File diff suppressed because one or more lines are too long

20
README
View File

@ -1,9 +1,17 @@
TiLDA Mk4 App Library
-------------------
* How to use tilda_tools.py
* How to transfer this to badge
* How to fork
* How to make your own changes
* How to run tests
* How to send a PR
Check out the wiki: https://badge.emfcamp.org/wiki/TiLDA_MK4
Please use one of these categories:
* System
* Homescreens
* Games
* Sound
* EMF
* Villages
* Phone
* LEDs
* Sensors
* Demo
* Other

180
SketchyEtch/main.py Normal file
View File

@ -0,0 +1,180 @@
"""Accidentally created etcher sketch...\nThen made it awesome"""
___name___ = "Sketchy-Etch"
___title___ = "Sketchy-Etch"
___license___ = "MIT"
___dependencies___ = ["ugfx_helper", "dialogs"]
___categories___ = ["Games"]
import ugfx, ugfx_helper, app, dialogs
from tilda import Buttons
from time import sleep
def reset():
global i
global j
global maxHeight
i = int(ugfx.width() / 2)
j = int(maxHeight / 2)
ugfx.area(0, 0, ugfx.width(), maxHeight, ugfx.BLACK)
ugfx.area((i - 1) if i > 0 else 0, (j - 1) if j > 0 else 0, 3 if (i > 0 and i < (ugfx.width() - 1)) else 2, 3 if (j > 0 and j < (maxHeight - 1)) else 2, ugfx.GREY)
def getColour(intensity, angle):
intensity *= 2
if angle < (1 / 6):
return (intensity, intensity * (angle * 6), 0) if intensity < 1 else (1, (angle * 6) + ((1 - (angle * 6)) * (intensity - 1)), (intensity - 1))
elif angle < (2 / 6):
return (intensity * (2 - (6 * angle)), intensity, 0) if intensity < 1 else ((2 - (6 * angle)) + ((1 - (2 - (6 * angle))) * (intensity - 1)), 1, (intensity - 1))
elif angle < (3 / 6):
return (0, intensity, intensity * ((6 * angle) - 2)) if intensity < 1 else ((intensity - 1), 1, ((6 * angle) - 2) + ((1 - ((6 * angle) - 2)) * (intensity - 1)))
elif angle < (4 / 6):
return (0, intensity * (4 - (6 * angle)), intensity) if intensity < 1 else ((intensity - 1), (4 - (6 * angle)) + ((1 - (4 - (6 * angle))) * (intensity - 1)), 1)
elif angle < (5 / 6):
return (intensity * ((6 * angle) - 4), 0, intensity) if intensity < 1 else (((6 * angle) - 4) + ((1 - ((6 * angle) - 4)) * (intensity - 1)), (intensity - 1), 1)
else:
return (intensity, 0, intensity * 6 * (1 - angle)) if intensity < 1 else (1, (intensity - 1), (6 * (1 - angle)) + ((1 - (6 * (1 - angle))) * (intensity - 1)))
shades = 16
hues = 20
scroll = 0
huesToShow = 2
colourI = 0
colourJ = 0
def showColourChangeMenu():
global shades
global hues
global scroll
global huesToShow
global maxHeight
boxHeight = int((ugfx.height() - maxHeight) / huesToShow)
boxWidth = int(ugfx.width() / shades)
for x in range(shades):
for y in range(scroll, scroll + huesToShow):
(r, g, b) = getColour(x / shades, y / hues)
ugfx.area(x * boxWidth, maxHeight + int((y - scroll) * boxHeight), boxWidth, boxHeight, (int(31 * r) << 11) + (int(63 * g) << 5) + int(31 * b))
def selectColour():
global shades
global hues
global scroll
global huesToShow
global colourI
global colourJ
global maxHeight
boxHeight = int((ugfx.height() - maxHeight) / huesToShow)
boxWidth = int(ugfx.width() / shades)
(r, g, b) = getColour(colourI / shades, colourJ / hues)
ugfx.box(colourI * boxWidth, maxHeight + ((colourJ - scroll) * boxHeight), boxWidth, boxHeight, (int(31 * (1 - r)) << 11) + (int(63 * (1 - g)) << 5) + int(31 * (1 - b)))
while not Buttons.is_pressed(Buttons.JOY_Center):
positionChanged = False
scrollChanged = False
oldI = colourI
oldJ = colourJ
if Buttons.is_pressed(Buttons.JOY_Right) and (colourI < (shades - 1)):
colourI += 1
positionChanged = True
while Buttons.is_pressed(Buttons.JOY_Right):
pass
elif Buttons.is_pressed(Buttons.JOY_Left) and (colourI > 0):
colourI -= 1
positionChanged = True
while Buttons.is_pressed(Buttons.JOY_Left):
pass
if Buttons.is_pressed(Buttons.JOY_Down) and (colourJ < (hues - 1)):
if (colourJ - scroll) == 1:
scroll += 1
scrollChanged = True
colourJ += 1
positionChanged = True
while Buttons.is_pressed(Buttons.JOY_Down):
pass
elif Buttons.is_pressed(Buttons.JOY_Up) and (colourJ > 0):
if (colourJ - scroll) == 0:
scroll -= 1
scrollChanged = True
colourJ -= 1
positionChanged = True
while Buttons.is_pressed(Buttons.JOY_Up):
pass
if scrollChanged or positionChanged:
if scrollChanged:
showColourChangeMenu()
elif positionChanged:
(r, g, b) = getColour(oldI / shades, oldJ / hues)
ugfx.box(oldI * boxWidth, maxHeight + ((oldJ - scroll) * boxHeight), boxWidth, boxHeight, (int(31 * r) << 11) + (int(63 * g) << 5) + int(31 * b))
(r, g, b) = getColour(colourI / shades, colourJ / hues)
ugfx.box(colourI * boxWidth, maxHeight + ((colourJ - scroll) * boxHeight), boxWidth, boxHeight, (int(31 * (1 - r)) << 11) + (int(63 * (1 - g)) << 5) + int(31 * (1 - b)))
sleep(0.05)
while Buttons.is_pressed(Buttons.JOY_Center):
pass
(r, g, b) = getColour(colourI / shades, colourJ / hues)
ugfx.box(colourI * boxWidth, maxHeight + ((colourJ - scroll) * boxHeight), boxWidth, boxHeight, (int(31 * r) << 11) + (int(63 * g) << 5) + int(31 * b))
return (int(31 * r) << 11) + (int(63 * g) << 5) + int(31 * b)
ugfx_helper.init()
maxHeight = int(ugfx.height() * 0.9)
i = 0
j = 0
ugfx.clear()
dialogs.notice("Draw with joystick arrows\nHold joystick centre for circle\nA to clear\nMENU to choose colour\nB to exit", title="Sketchy-Etch")
ugfx.area(0, 0, ugfx.width(), maxHeight, ugfx.BLACK)
showColourChangeMenu()
circleSize = 3
reset()
colour = ugfx.WHITE
while not Buttons.is_pressed(Buttons.BTN_B):
changed = False
oldI = i
oldJ = j
if Buttons.is_pressed(Buttons.JOY_Right) and (i < (ugfx.width() - 1)):
i += 1
changed = True
elif Buttons.is_pressed(Buttons.JOY_Left) and (i > 0):
i -= 1
changed = True
if Buttons.is_pressed(Buttons.JOY_Down) and (j < (maxHeight - 1)):
j += 1
changed = True
elif Buttons.is_pressed(Buttons.JOY_Up) and (j > 0):
j -= 1
changed = True
if Buttons.is_pressed(Buttons.JOY_Center):
circleSize += 1
ugfx.fill_circle(i, j, circleSize, colour)
showColourChangeMenu()
if Buttons.is_pressed(Buttons.BTN_A):
circleSize = 3
reset()
if Buttons.is_pressed(Buttons.BTN_Menu):
colour = selectColour()
circleSize = 3
if changed:
circleSize = 3
ugfx.area((oldI - 1) if oldI > 0 else 0, (oldJ - 1) if oldJ > 0 else 0, 3 if (oldI > 0 and oldI < (ugfx.width() - 1)) else 2, 3 if (oldJ > 0 and oldJ < (maxHeight - 1)) else 2, colour)
ugfx.area((i - 1) if i > 0 else 0, (j - 1) if j > 0 else 0, 3 if (i > 0 and i < (ugfx.width() - 1)) else 2, 3 if (j > 0 and j < (maxHeight - 1)) else 2, ugfx.GREY)
sleep(0.05)
ugfx.clear()
app.restart_to_default()

8
adhoc.py Normal file
View File

@ -0,0 +1,8 @@
# Fancy a quick experiment? No problem, just code it in here and run
# "./tilda_tools run adhoc.py"
import ugfx, ugfx_helper, dialogs
ugfx_helper.init()
ugfx.clear()

198
air_quality/main.py Normal file
View File

@ -0,0 +1,198 @@
"""This app needs an SDS011 sensor attacthed to UART 4 """
___name___ = "Air Quality"
___license___ = "MIT"
___dependencies___ = ["sleep", "app", "ugfx_helper", "buttons", "homescreen"]
___categories___ = ["EMF"]
___bootstrapped___ = False # Whether or not apps get downloaded on first install. Defaults to "False", mostly likely you won't have to use this at all.
import app
import ugfx, os, time, sleep
from tilda import Buttons
from tilda import Sensors
from machine import Pin
from machine import UART
from machine import Neopix
import random
class DustSensorTester(object):
verbose = False
def contains_sequence (self,data, test):
""" Checks to see if the data sequence contains the test sewquence
Args:
data: sequence of data items
test: test sequence
Returns:
True if the test sequence is in the data sequence
"""
if len(data)<len(test): return False
for pos in range(0,len(data)-len(test)+1):
if test == data[pos:pos+len(test)]: return True
return False
class DustSensor(object):
verbose = False
fake_sensor = False
AWAITING_START = 0
READING_BLOCK = 1
def pump_byte(self, b):
""" Pump a byte into the block decode. Calls the decode method
when the block is complete
Args:
b: byte to pump
"""
if self.state==self.AWAITING_START:
if self.verbose: print(self, "Awaiting start:", b)
if b==self.start_sequence[self.start_pos]:
# got a match - move to next byte in start sequence
self.start_pos = self.start_pos+1
if self.start_pos == len(self.start_sequence):
# matched the start sequence
self.block=self.start_sequence.copy()
self.state=self.READING_BLOCK
elif self.state==self.READING_BLOCK:
if self.verbose: print("Reading block:", b)
self.block.append(b)
if len(self.block) == self.block_size:
self.state=self.AWAITING_START
self.start_pos=0
self.process_block()
def __init__(self, display):
self.display = display
self.state=self.AWAITING_START
self.start_pos = 0
class sds011_sensor(DustSensor):
start_sequence = [0xaa,0xc0]
block_size = 10
def process_block(self):
""" Process a block of data obtained from the sensor
calls the new_reading method on the display to
deliver a new reading or the error method
on the display to indicate an error
"""
if self.verbose: print("sds011 process block")
if self.verbose: print([hex(x) for x in self.block])
check_sum = 0
for i in range(2,8):
check_sum = check_sum + self.block[i]
check_sum = check_sum & 0xff
if self.verbose: print("Checksum:",hex(check_sum))
if check_sum!=self.block[8]:
message = "Rcv:" + hex(self.block[8]) + " Cal:" + hex(check_sum)
self.display.error(message)
return
ppm10 = (self.block[4]+256*self.block[5])/10
ppm2_5 = (self.block[2]+256*self.block[3])/10
self.display.new_readings(ppm10,ppm2_5)
class Air_Quality_Display():
def setup_screen(self):
""" Set up the screen and the labels that display
values on it.
"""
ugfx.init()
width=ugfx.width()
height=ugfx.height()
ugfx.clear(ugfx.html_color(0x800080))
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x800080), ugfx.html_color(0x800080), ugfx.html_color(0x800080)])
style.set_background(ugfx.html_color(0x800080))
ugfx.set_default_style(style)
ugfx.orientation(90)
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, 0, width, 60,"Air Quality", justification=ugfx.Label.CENTER)
label_height=45
self.ppm10_label = ugfx.Label(0, label_height, width, label_height,"PPM 10: starting", justification=ugfx.Label.CENTER)
self.ppm25_label = ugfx.Label(0, label_height*2, width, label_height,"PPM 2.5: starting", justification=ugfx.Label.CENTER)
self.temp_label = ugfx.Label(0, label_height*3, width, label_height,"Temp: starting", justification=ugfx.Label.CENTER)
self.humid_label = ugfx.Label(0, label_height*4, width, label_height,"Humid: starting", justification=ugfx.Label.CENTER)
self.error_label = ugfx.Label(0, label_height*5, width, label_height,"", justification=ugfx.Label.CENTER)
self.message_label = ugfx.Label(0, label_height*6, width, label_height,"", justification=ugfx.Label.CENTER)
self.error_count = 0
self.error_message = ""
self.neopix = Neopix()
self.p10_decode = ((100,0x00ff00),(250,0xffff00),(350,0xff8000),(430,0xff0000),(-1,0xcc6600))
self.p25_decode = ((60, 0x00ff00),(91, 0xffff00), (121,0xff8000),(251,0xff0000),(-1,0xcc6600))
def get_reading_color(self, value, decode):
for item in decode:
if item[0] < 0:
# reached the upper limit - return
return item[1]
if value < item[0]:
return item[1]
def new_readings(self,ppm10_value, ppm25_value):
""" Called by the sensor to deliver new values to the screen.
Will also trigger the reading of the temperature and humidity
values.
"""
self.ppm10_label.text("PPM 10: "+str(ppm10_value))
self.ppm25_label.text("PPM 2.5: "+str(ppm25_value))
temp = Sensors.get_hdc_temperature()
temp_string = "Temp: {0:2.1f}".format(temp)
self.temp_label.text(temp_string)
humid = Sensors.get_hdc_humidity()
humid_string = "Humidity: {0:2.1f}".format(humid)
self.humid_label.text(humid_string)
# Calculate some colours
self.neopix.display((self.get_reading_color(ppm25_value, self.p25_decode),self.get_reading_color(ppm10_value, self.p10_decode)))
def error(self, error_message):
""" Called by the sensor to deliver an error message.
Args:
error_message: error message string
"""
self.error_count = self.error_count + 1
self.error_label.text( "Errors: " +str(self.error_count))
self.message_label.text(str(error_message))
display = Air_Quality_Display()
display.setup_screen()
sensor_port = UART(2,9600, bits=8, mode=UART.BINARY, parity=None, stop=1)
sensor = sds011_sensor(display)
def test_sensor():
""" Can be called to pump some test sequences into the sensor
"""
test_sequences = [ ['0xaa', '0xc0', '0xf', '0x0', '0x22', '0x0', '0xe1', '0xdb', '0xed', '0xab'],
['0xaa', '0xc0', '0x13', '0x0', '0x3e', '0x0', '0xe1', '0xdb', '0xb', '0xab'], # bad checksum
['0xaa', '0xc0', '0x13', '0x0', '0x3e', '0x0', '0xe1', '0xdb', '0xa', '0xab'],
['0xaa', '0xc0', '0x13', '0x0', '0x3e', '0x0', '0xe1', '0xdb', '0xd', '0xab'] ]
for test_sequence in test_sequences:
for ch in test_sequence:
sensor.pump_byte(int(ch))
# test_sensor()
buffer = bytearray([0])
while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
while sensor_port.any() > 0:
sensor_port.readinto(buffer,1)
sensor.pump_byte(buffer[0])
sleep.wfi()
ugfx.clear()
app.restart_to_default()

160
avatar/main.py Normal file
View File

@ -0,0 +1,160 @@
"""A simple homescreen diplaying an avatar from an url and the user's name"""
___title___ = "Avatar Homescreen"
___license___ = "WTFPL"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "wifi", "http", "sleep", "app", "buttons"]
___bootstrapped___ = False
___launchable___ = True
import ugfx_helper, uos, wifi, ugfx, http, time, sleep, app, sys, database, buttons
from tilda import Buttons
from homescreen import *
from dialogs import *
# Constants
intro_height = 30
name_height = 60
status_height = 20
info_height = 30
max_name = 8
avatar_file_name='shared/avatar.png'
avatar_db_key="avatar_url"
# Local variables
db = database.Database()
### START OF WRITING STUFF ###
def write_instructions():
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(270)
ugfx.text(5, 5, "Press A to refresh", ugfx.WHITE)
ugfx.text(5, 25, "Press B to change the url", ugfx.WHITE)
ugfx.text(5, 45, "Press Menu to exit", ugfx.WHITE)
def write_hot_instructions():
ugfx.orientation(270)
ugfx.text(3, 85, "Press A to refresh or press B", ugfx.WHITE)
ugfx.text(3, 105, "to change the url or check", ugfx.WHITE)
ugfx.text(3, 125, "your wifi settings...", ugfx.WHITE)
def write_loading():
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(90)
ugfx.text(5, 5, "Loading...", ugfx.WHITE)
ugfx.orientation(270)
ugfx.text(5, 5, "Loading...", ugfx.WHITE)
def write_name():
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.orientation(90)
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER, style=style)
### END OF WRITING STUFF ###
### START OF AVATAR HANDLING STUFF ###
def avatar_exists():
ret = True
try:
f = open(avatar_file_name, 'r')
except:
ret = False
return ret
def load_avatar():
#Load the avatar from the local storage
try:
f = open(avatar_file_name, 'r')
avatar_file = f.read()
ugfx.orientation(90)
ugfx.display_image(0,0,bytearray(avatar_file))
f.close()
return True
except:
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(270)
ugfx.text(3, 65, "No local avatar.", ugfx.RED)
return False
def download_avatar():
avatar_url=db.get("avatar_url", "")
if avatar_url:
if (avatar_url.endswith(".png") or avatar_url.startswith("http")):
try:
image = http.get(avatar_url).raise_for_status().content
ugfx.orientation(90)
ugfx.display_image(0,0,bytearray(image))
#f = open(avatar_file_name, 'w')
#f.write(image)
#f.close()
#ugfx.display_image(0,0,bytearray(image))
except:
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(270)
ugfx.text(3, 65, "Couldn't download the avatar.", ugfx.RED)
return False
else:
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(270)
ugfx.text(3, 65, "Invalid avatar url.", ugfx.RED)
return False
else:
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(270)
ugfx.text(3, 65, "No avatar url.", ugfx.RED)
return True
### END OF AVATAR HANDLING STUFF ###
### START OF MAIN ###
def start():
write_name()
#if not avatar_exists():
if not download_avatar():
write_hot_instructions()
#if not load_avatar():
#write_hot_instructions()
init()
ugfx.clear(ugfx.html_color(0x000000))
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x000000), ugfx.html_color(0x000000), ugfx.html_color(0x000000)])
style.set_background(ugfx.html_color(0x000000))
ugfx.set_default_style(style)
write_instructions()
wait_until = time.ticks_ms() + 3000
while time.ticks_ms() < wait_until:
time.sleep(0.1)
if Buttons.is_pressed(Buttons.BTN_A) or Buttons.is_pressed(Buttons.BTN_B) or Buttons.is_pressed(Buttons.BTN_Menu):
break
start()
while True:
if buttons.is_triggered(Buttons.BTN_B):
ugfx.orientation(270)
avatar_url = prompt_text("Avatar url:", init_text=db.get(avatar_db_key, ""))
db.set(avatar_db_key, avatar_url)
db.flush()
ugfx.orientation(90)
start()
if buttons.is_triggered(Buttons.BTN_Menu):
break
app.restart_to_default()
### END OF MAIN ###

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -6,65 +6,129 @@ To publish apps use https://badge.emfcamp.org"""
___license___ = "MIT"
___title___ = "Badge Store"
___dependencies___ = ["wifi", "dialogs"]
___dependencies___ = ["badge_store", "dialogs", "ugfx_helper", "app", "database", "ospath"]
___categories___ = ["System"]
___bootstrapped___ = True
import pyb
import ugfx
import os
#import http_client
import wifi
import dialogs
#from app import App, get_local_apps, get_public_apps, get_public_app_categories, empty_local_app_cache
#import filesystem
TEMP_FILE = ".temp_download"
ugfx.init()
import ugfx_helper, os, database, wifi, app, ospath
from dialogs import *
from lib.badge_store import BadgeStore
from app import *
### VIEWS ###
ugfx_helper.init()
url = database.get("badge_store.url", "http://badgeserver.emfcamp.org/2018")
repo = database.get("badge_store.repo", "emfcamp/Mk4-Apps")
ref = database.get("badge_store.ref", "master")
store = BadgeStore(url=url, repo=repo, ref=ref)
title = "TiLDA Badge Store"
def clear():
ugfx.clear(ugfx.html_color(0x7c1143))
ugfx.clear()
def store():
None
def show_categories():
clear()
with WaitingMessage(title=title, text="Loading categories..."):
menu_items = [{"title": c, "category": c} for c in store.get_categories()]
def update():
None
option = prompt_option(menu_items, none_text="Back", title="Install: Categories")
def remove():
None
if option:
show_apps(option["category"])
else:
return
def settings():
None
def show_apps(c):
clear()
menu_items = [{"title": a, "app": a} for a in store.get_apps(c)]
option = prompt_option(menu_items, none_text="Back", title="Install: " + c)
if option:
show_app(option["app"],c)
else:
show_categories()
def show_app(a,c):
clear()
with WaitingMessage(title=title, text="Loading app description..."):
app_info = store.get_app(a)
# Try to get the 'title' key from app_info, falling back to the value of a if not present
name = app_info.get("title", a)
desc = app_info["description"].strip()
app_text = """App:\n{}\n\nDescription:\n{}""".format(name, desc)
install = prompt_boolean(app_text , title="Install App", true_text="Install", false_text="Back")
if install:
app_text = "App:\n{}\n\n".format(name)
with WaitingMessage(title="Installing App...", text="%sGetting ready..." % app_text) as message:
installers = store.install([a])
n = len(installers)
for i, installer in enumerate(installers):
message.text = "%s%s (%s/%s)" % (app_text + "Downloading files...\n\n", installer.path, i + 1, n)
installer.download()
app.uncache_apps()
launch = prompt_boolean(
"%sSuccessfully installed.\n\nPress A to launch the app.\n\nPress B to list more \"%s\" apps." % (app_text, c), title="Install Success!", true_text="Launch", false_text="Back")
if (launch):
for app_obj in get_apps():
if app_obj.name == a:
app_obj.boot()
else:
show_apps(c)
else:
show_apps(c)
def show_update():
clear()
update = prompt_boolean("Do you want to update all apps on this badge?", title="Update all Apps", true_text="OK", false_text="Back")
if update:
clear()
with WaitingMessage(title=title, text="Getting updates...") as message:
update_text = "Downloading files:"
installers = store.install(_get_current_apps())
n = len(installers)
for i, installer in enumerate(installers):
message.text = "%s\n\n%s (%s/%s)" % (update_text, installer.path, i + 1, n)
installer.download()
notice("Your badge has been successfully updated.", title="Update Success!", close_text="Back")
def show_remove():
clear()
app_to_remove = prompt_option(_get_current_apps(), title="Remove App...", none_text="Back", text="Select an App to remove.")
if app_to_remove:
ospath.recursive_rmdir(app_to_remove)
app.uncache_apps()
app_text = """App:\n{}""".format(app_to_remove)
notice("\"%s\"\n\nThe app has now been removed." % app_text, title="Remove Success!", close_text="Back")
def main_menu():
while True:
clear()
print()
menu_items = [
{"title": "Install Apps", "function": store},
{"title": "Update", "function": update},
{"title": "Manage Apps", "function": remove},
{"title": "Settings", "function": settings}
{"title": "Install Apps", "function": show_categories},
{"title": "Update all Apps", "function": show_update},
{"title": "Remove App", "function": show_remove}
]
option = dialogs.prompt_option(menu_items, none_text="Exit", text="What do you want to do?", title="TiLDA App Library")
option = prompt_option(menu_items, none_text="Exit", text="What do you want to do?", title=title)
if option:
option["function"]()
else:
return
break
def _get_current_apps():
return [a.name for a in app.get_apps()]
wifi.connect(show_wait_message=True)
main_menu()
#if App("home").loadable:
# main_menu()
#else:
# for app_name in ["changename", "snake", "alistair~selectwifi", "sponsors", "home"]:
# install(App(app_name))
# pyb.hard_reset()
app.restart_to_default()

25
badgesimulator/main.py Normal file
View File

@ -0,0 +1,25 @@
"""This app creates a real EMF badge experience"""
___title___ = "EMF 2018 badge simulator"
___license___ = "MIT"
___categories___ = ["EMF"]
___dependencies___ = ["sleep", "app"]
import ugfx, app
from time import sleep
from tilda import Buttons
ugfx.init()
ugfx.clear()
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
ugfx.Label(10, 10, 240, 15, "EMF2018")
ugfx.Label(10, 40, 240, 15, "TiLDA Mk4")
ugfx.Label(10, 80, 240, 15, "Error")
ugfx.Label(10, 110, 240, 15, "Something went wrong :(")
while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
sleep(2)
ugfx.clear()
app.restart_to_default()

47
basic_clock/main.py Normal file
View File

@ -0,0 +1,47 @@
"""An NTP time app"""
___title___ = "NTP time"
___license___ = "MIT"
___dependencies___ = ["ntp", "wifi", "app"]
___categories___ = ["EMF"]
# borrowed from https://github.com/micropython/micropython/blob/master/esp8266/scripts/ntptime.py
import ugfx, ntp, wifi, utime, machine, app
from tilda import Buttons
# initialize screen
ugfx.init()
ugfx.clear()
# set the RTC using time from ntp
# print out RTC datetime
if not wifi.is_connected():
wifi.connect(show_wait_message=True)
ntp.set_NTP_time()
rtc = machine.RTC()
ugfx.orientation(270)
count = 0
last = None
while 1:
now = rtc.now()[:6]
year = now[0]
month = now[1]
day = now[2]
hour = now[3]
minute = now[4]
second = now[5]
if now != last:
last = now
ugfx.clear()
ugfx.text(5, 5, "current time", ugfx.BLACK)
time_str = "%02i:%02i:%02i %i/%i/%4i" % (hour, minute, second, day, month, year)
ugfx.text(5, 20, time_str, ugfx.BLACK)
if Buttons.is_pressed(Buttons.BTN_A) or Buttons.is_pressed(Buttons.BTN_B) or Buttons.is_pressed(Buttons.BTN_Menu):
break
utime.sleep_ms(10)
ugfx.clear()
app.restart_to_default()

89
beer/main.py Normal file
View File

@ -0,0 +1,89 @@
"""What's on tap?!
Get up to date information on what's in stock at The Robot Arms!
"""
___title___ = "beer"
___license___ = "MIT"
___dependencies___ = ["app", "sleep", "wifi", "http", "ugfx_helper"]
___categories___ = ["EMF"]
import wifi, ugfx, http, ujson, app, sleep
from tilda import Buttons, LED
orientation = 270
def get_beer():
global bar, stock
LED(LED.RED).on()
try:
bar_json = http.get("https://bar.emf.camp/location/Bar.json").raise_for_status().content
stock_json = http.get("https://bar.emf.camp/stock.json").raise_for_status().content
bar = ujson.loads(bar_json)
stock = ujson.loads(stock_json)
except:
print('oh poop')
LED(LED.RED).off()
draw_screen()
def draw_screen():
global bar, stock
ugfx.clear(ugfx.BLACK)
ugfx.text(65, 5, "what's on tap?", ugfx.RED)
ugfx.line(5, 20, ugfx.width(), 20, ugfx.GREY)
for idx, beer in enumerate(bar['location']):
remaining = 0
for item in stock['stock']:
if item['description'] == beer['description']:
remaining = float(item['remaining'])
ugfx.text(5, 22 + idx*15, beer['description'][:28], ugfx.WHITE)
ugfx.text(202, 22 + idx*15, '!' if (remaining < 30) else ' ', ugfx.RED)
ugfx.text(210, 22 + idx*15, "{:>4}".format(beer['price']), ugfx.WHITE)
def toggle_orientation():
global orientation
if orientation == 90:
ugfx.orientation(270)
orientation = 270
draw_screen()
else:
ugfx.orientation(90)
orientation = 90
draw_screen()
ugfx.init()
ugfx.clear(ugfx.BLACK)
ugfx.set_default_font(ugfx.FONT_FIXED)
s=ugfx.Style()
s.set_enabled([ugfx.WHITE, ugfx.BLACK, ugfx.BLACK, ugfx.GREY])
s.set_background(ugfx.BLACK)
ugfx.set_default_style(s)
Buttons.enable_interrupt(Buttons.BTN_A, lambda button_id:get_beer(), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_B, lambda button_id:toggle_orientation(), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_Menu, lambda button_id:app.restart_to_default(), on_press=True, on_release=False)
ugfx.text(5, 10, "Instructions:", ugfx.WHITE)
ugfx.text(5, 30, "Press the A button to refresh", ugfx.WHITE)
ugfx.text(5, 45, "Press the B button to rotate", ugfx.WHITE)
ugfx.text(5, 60, "Press the Menu button to exit", ugfx.WHITE)
ugfx.text(5, 90, "!", ugfx.RED)
ugfx.text(15, 90, "means the stock is low", ugfx.WHITE)
ugfx.text(5, 120, "Loading data from the bar...", ugfx.WHITE)
get_beer()
while True:
sleep.wfi()
ugfx.clear()
app.restart_to_default()

208
bf-interpreter/main.py Normal file
View File

@ -0,0 +1,208 @@
"""Simple brainfuck (an esoteric programming language) interpreter.
Runs very slowly... prints sierpinski triangle"""
___name___ = "bf interpreter"
___license___ = "MIT"
___dependencies___ = ["sleep", "app"]
___categories___ = ["Other"]
import ugfx, os, time, sleep, app
from tilda import Buttons
from time import sleep_ms
# initialize screen
ugfx.init()
ugfx.clear()
ugfx.set_default_font(ugfx.FONT_TITLE)
Prog="""
+>-[>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>++[-]->>>>+>>>+>>>+>>>+>>>+>
>>+>>>+>>>+>>>++[-<+]-<<<<+<<++++[->++++++++<]<++[------>+<]>++<<+[--->++<]>++
<<-[--->+<]>------<+[-<+]-<[>>+[->+]-<[-]<<[-]+++[>[-]++++++++++.[-]+++[>+[>>+<<
-]>>[<<++[-<+]->++[->+]->-]<<+[-<+]->-[-[-[-[-[-[-[-[->>>]>>>]>>>]>>>]>>>]>>>]>>
>]>>>]>>>>>>>>>>>>>>>>>>>>> > > > > > > > > > > > > >>>>>>>>>>[+[-<+]-<<<<<<.>>>
>>>>]>[+[-<+]-<<<< <<<.>>>>>>>>]>[+[-
<+]-<<<<<<<<.>>> tic tac toe >>>>>>]+[-<+]-<<
<<<.>>>-]<-]+++ to play: type a number (1 to 9) to +++++++.[-]<<<<
<<[<<<<<<<<<<<+ place an X at that grid location [--->++<]>+++.[
->+++++++<]>.++ ++++.-[---->+<]
>+++.---[->+++<] [ http://mitxela.com/ ] >.+++[->++++<]>+
.+++++.-[->+++++<] >.[--->+<]>-.+[-<+
]-<[-]>>>>]<[<<<<<<<++++[++++>---<]>+.[++++>---<]>-.+++[->+++<]>++.+[--->+<]>+.+
[---->+<]>+++.[--->+<]>-.[-]+[-<+]-<[-]>>>>]<[<<<<<<<<<<+[--->++<]>+++.[->++++++
+<]>.++++++.-[---->+<]>+++.++++++[->++<]>.+[--->+<]>.++++.++++[->+++<]>.--[--->+
<]>.[--->+<]>-.+[-<+]-<[-]>>>>]<+[-<+]-<[>>->>>>>>+[-<<<<[-]<<[-]>>>>-[>>[-]+<<+
<[-]<[-]<[-]<[-]-[----->+<]>---<,>[-<->]<[>>+>+<<<-]>>[<<+>>-]+++++++++[->-[<<]>
]>>-]<<<<[-]>>>>[-]+<<<<<<[>>+>+<<<-]>>[<<+>>-]>>]>>-<<<[-]<<[<->-]<-[-[-[-[-[-[
-[-[->>>]>>>]>>>]>>>]>>>]>>>]>>>]>>>]]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>[->++[-<+]->>>>>[>>>[>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>>>>[>
>>[>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>>>>>>>>>>>>>[>>>[>>>[+[-
<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<[-]
++[->+]->]]]+[-<+]->>>>>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+
[-<+]->>>>>>>>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>
>[>>>>>>>>>>>>[>>>>>>>>>>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>[>>
>>>>[>>>>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]-<<<<<<<<<-[++[->+]-<<<<<<<<<<[
-]++[->+]->>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]+[-<+]->>>>>>>[+[-<+]-<<<<<<<<<<[-]+
[->+]->]+[-<+]->>>>>>>>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]+[-<+]->>>>>>>>>>>>>[+[-<
+]-<<<<<<<<<<[-]+[->+]->]+[-<+]->>>>>>>>>>>>>>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]+[
-<+]->>>>>>>>>>>>>>>>>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]+[-<+]->>>>>>>>>>>>>>>>>>>
>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>>[+[-<+]-<<<<<<<
<<<[-]+[->+]->]+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>>>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]
+[-<+]-<<[-]>[-]+>>>>>>[>>>[>>[+[-<+]-<[-]<[-]++++[->+]->]]]>+[-<+]->>>>>[>>[>>>
>[+[-<+]-<[-]<[-]+++[->+]->]]]>+[-<+]->>>>[>>>>[>>>[+[-<+]-<[-]<[-]++[->+]->]]]>
+[-<+]->>>>>>>>>>>>>>[>>>[>>[+[-<+]-<[-]<[-]+++++++[->+]->]]]>+[-<+]->>>>>>>>>>>
>>>[>>[>>>>[+[-<+]-<[-]<[-]++++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>[>>>>[>>>[+[-<+]
-<[-]<[-]+++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>>[>>>[>>[+[-<+]-<[-]<[-]++
++++++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>>[>>[>>>>[+[-<+]-<[-]<[-]+++++++
++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>[>>>>[>>>[+[-<+]-<[-]<[-]++++++++[->+]
->]]]>+[-<+]->>>>>[>>>>>>>>>[>>>>>>>>[+[-<+]-<[-]<[-]++++++++[->+]->]]]>+[-<+]->
>>>>[>>>>>>>>[>>>>>>>>>>[+[-<+]-<[-]<[-]+++++[->+]->]]]>+[-<+]->>>>[>>>>>>>>>>[>
>>>>>>>>[+[-<+]-<[-]<[-]++[->+]->]]]>+[-<+]->>>>>>>>[>>>>>>>>>[>>>>>>>>[+[-<+]-<
[-]<[-]+++++++++[->+]->]]]>+[-<+]->>>>>>>>[>>>>>>>>[>>>>>>>>>>[+[-<+]-<[-]<[-]++
++++[->+]->]]]>+[-<+]->>>>>>>[>>>>>>>>>>[>>>>>>>>>[+[-<+]-<[-]<[-]+++[->+]->]]]>
+[-<+]->>>>>>>>>>>[>>>>>>>>>[>>>>>>>>[+[-<+]-<[-]<[-]++++++++++[->+]->]]]>+[-<+]
->>>>>>>>>>>[>>>>>>>>[>>>>>>>>>>[+[-<+]-<[-]<[-]+++++++[->+]->]]]>+[-<+]->>>>>>>
>>>[>>>>>>>>>>[>>>>>>>>>[+[-<+]-<[-]<[-]++++[->+]->]]]>+[-<+]->>>>>[>>>>>>>>>>>>
[>>>>>>>>>>>[+[-<+]-<[-]<[-]++++++++++[->+]->]]]>+[-<+]->>>>[>>>>>>>>>>>>>[>>>>>
>>>>>>>[+[-<+]-<[-]<[-]++[->+]->]]]>+[-<+]->>>>>>>>>>>[>>>>>>[>>>>>[+[-<+]-<[-]<
[-]++++++++[->+]->]]]>+[-<+]->>>>>>>>>>[>>>>>>>[>>>>>>[+[-<+]-<[-]<[-]++++[->+]-
>]]]>+[-<+]->>>>>>[>>>[>[+[-<+]-<[-]<[-]++++[->+]->]]]>+[-<+]->>>>>>[>[>>>>>[+[-
<+]-<[-]<[-]+++[->+]->]]]>+[-<+]->>>>[>>>>>[>>>[+[-<+]-<[-]<[-]++[->+]->]]]>+[-<
+]->>>>>>>>>>>>>>>[>>>[>[+[-<+]-<[-]<[-]+++++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>
[>[>>>>>[+[-<+]-<[-]<[-]++++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>[>>>>>[>>>[+[-<+]-<
[-]<[-]+++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>[>>>[>[+[-<+]-<[-]<[-]++++
++++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>[>[>>>>>[+[-<+]-<[-]<[-]++++++++
+[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>[>>>>>[>>>[+[-<+]-<[-]<[-]++++++++[->+]
->]]]>+[-<+]->>>>>>[>>>>>>>>>[>>>>>>>[+[-<+]-<[-]<[-]++++++++[->+]->]]]>+[-<+]->
>>>>>[>>>>>>>[>>>>>>>>>>>[+[-<+]-<[-]<[-]+++++[->+]->]]]>+[-<+]->>>>[>>>>>>>>>>>
[>>>>>>>>>[+[-<+]-<[-]<[-]++[->+]->]]]>+[-<+]->>>>>>>>>[>>>>>>>>>[>>>>>>>[+[-<+]
-<[-]<[-]+++++++++[->+]->]]]>+[-<+]->>>>>>>>>[>>>>>>>[>>>>>>>>>>>[+[-<+]-<[-]<[-
]++++++[->+]->]]]>+[-<+]->>>>>>>[>>>>>>>>>>>[>>>>>>>>>[+[-<+]-<[-]<[-]+++[->+]->
]]]>+[-<+]->>>>>>>>>>>>[>>>>>>>>>[>>>>>>>[+[-<+]-<[-]<[-]++++++++++[->+]->]]]>+[
-<+]->>>>>>>>>>>>[>>>>>>>[>>>>>>>>>>>[+[-<+]-<[-]<[-]+++++++[->+]->]]]>+[-<+]->>
>>>>>>>>[>>>>>>>>>>>[>>>>>>>>>[+[-<+]-<[-]<[-]++++[->+]->]]]>+[-<+]->>>>>>[>>>>>
>>>>>>>[>>>>>>>>>>[+[-<+]-<[-]<[-]++++++++++[->+]->]]]>+[-<+]->>>>[>>>>>>>>>>>>>
>[>>>>>>>>>>>>[+[-<+]-<[-]<[-]++[->+]->]]]>+[-<+]->>>>>>>>>>>>[>>>>>>[>>>>[+[-<+
]-<[-]<[-]++++++++[->+]->]]]>+[-<+]->>>>>>>>>>[>>>>>>>>[>>>>>>[+[-<+]-<[-]<[-]++
++[->+]->]]]>+[-<+]-<[>>+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>>>>>[+[-<+]-<[-]<[-]+++++
+++++[->+]->]+[-<+]->>>>>>>>>>>>>>>>>>>>>>[+[-<+]-<[-]<[-]++++++++[->+]->]+[-<+]
->>>>>>>>>>[+[-<+]-<[-]<[-]++++[->+]->]+[-<+]->>>>[+[-<+]-<[-]<[-]++[->+]->]+[-<
+]->>>>>>>>>>>>>>>>>>>>>>>>>[+[-<+]-<[-]<[-]+++++++++[->+]->]+[-<+]->>>>>>>>>>>>
>>>>>>>[+[-<+]-<[-]<[-]+++++++[->+]->]+[-<+]->>>>>>>>>>>>>[+[-<+]-<[-]<[-]+++++[
->+]->]+[-<+]->>>>>>>[+[-<+]-<[-]<[-]+++[->+]->]+[-<+]->>>>>>>>>>>>>>>>[+[-<+]-<
[-]<[-]++++++[->+]->]+[-<+]->]>>+[-<+]-<<<<[+[->+]->>>>>>>>>>>>>>>>>[+[-<+]-<[-]
<[-]++[->+]->]+[-<+]->]>>>>+[-<+]-<<[>>>+[-<+]-<[-]<[+[-<+]->++[->+]-<<-]+[-<+]-
>-[-[-[-[-[-[-[-[->>>]>>>]>>>]>>>]>>>]>>>]>>>]>>>]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>->>++[-<+]->]>>>>+[-<+]-<<[-]>>>+[-<+]-<<<<[-]>>>>>+[-<+]->>>>>>[>>
>[>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>>>>>[>>>[>>>[+[-<+]-<<<
<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>[>>>[>>>[+[-<+]-<<<<<<<<<<
<[-]++[->+]->]]]+[-<+]->>>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->
]]]+[-<+]->>>>>>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->]]]+[-<+]-
>>>>>>>>>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>[
>>>>>>>>>>>>[>>>>>>>>>>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>>[>
>>>>>[>>>>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->]]]+[-<+]-<[-]]++[->+]->]+[-<+]-<+[
-<+]-<]>>+[->+]->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>+[-[-]<+]-<+[-[-]<+]-<+>]
"""
# Hello World
#Prog="++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."
# Sierpinski
Prog="""
++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[
-<<<[
->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<<
]>.>+[>>]>+
]"""
buf=""
def output(t):
global buf
buf+=t
buf=buf[-(16*80):]
ugfx.clear()
lines=buf.split("\n")
lines=lines[-16:]
for i,v in enumerate(lines):
ugfx.text(5,i*20+5, v+" ", ugfx.BLACK)
lastpushed=0
def pushed(n):
global Tape, TP, waiting
if (waiting):
output(n+" \n")
Tape[TP]=ord(n)
waiting=False
Buttons.enable_interrupt(Buttons.BTN_1, lambda button_id:pushed("1"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_2, lambda button_id:pushed("2"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_3, lambda button_id:pushed("3"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_4, lambda button_id:pushed("4"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_5, lambda button_id:pushed("5"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_6, lambda button_id:pushed("6"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_7, lambda button_id:pushed("7"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_8, lambda button_id:pushed("8"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_9, lambda button_id:pushed("9"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_0, lambda button_id:pushed("0"), on_press=True, on_release=False)
output("Loading...")
waiting=False
Prog+='\0'
Tape=[0]*256
PP=-1
TP=0
while True:
if (waiting):
sleep_ms(200)
else:
PP=PP+1
if (PP>=len(Prog)):
waiting=True
output("END!")
elif (Prog[PP]=="+"):
Tape[TP]=Tape[TP]+1
elif (Prog[PP] =="-"):
Tape[TP]=Tape[TP]-1
elif (Prog[PP] ==">"):
TP=TP+1
elif (Prog[PP] =="<"):
TP=TP-1
elif (Prog[PP] =="."):
output(chr(Tape[TP]))
elif (Prog[PP] ==","):
waiting=True
elif (Prog[PP] =="["):
if (Tape[TP]==0):
depth=1
while (depth>0):
PP=PP+1
if (Prog[PP]=="]"):
depth = depth - 1
if (Prog[PP]=="["):
depth = depth + 1
elif (Prog[PP] =="]"):
if (Tape[TP]!=0):
depth=1
while (depth>0):
PP=PP-1
if (Prog[PP]=="]"):
depth = depth + 1
if (Prog[PP]=="["):
depth = depth - 1

141
bluetooth_speaker/main.py Normal file
View File

@ -0,0 +1,141 @@
"""App to use the badge as a (handset profile only) bluetooth speaker"""
___name___ = "Bluetooth Speaker"
___license___ = "MIT"
___dependencies___ = ["ugfx_helper", "sim800", "dialogs", "buttons", "app"]
___categories___ = ["Sound"]
import ugfx_helper, ugfx
import app
import sim800
from dialogs import *
import buttons
BLUETOOTH_NAME = "BadgeSpeaker"
g_paired = False
def pairing_dialog(scan_timeout_s=10):
''' Show BLE devices to pair with and connect. Returns True if paired, False if failed '''
waiting_message = WaitingMessage("Scanning for bluetooth devices for %s seconds"%scan_timeout_s, "Scanning")
devices = sim800.btscan(int(scan_timeout_s * 1000))
waiting_message.destroy()
# List format is [id, name, addr, rssi]. FIXME: Only returns 1 item?
try:
devices_prompts = [{'title': v[1], 'id': v[0]} for v in devices]
except TypeError: #Only one device found. #TODO: Not very neat
devices_prompts = [{'title':devices[1] ,'id':devices[0]},]
#TODO: Fix non printable chars in device names
option = prompt_option(devices_prompts, title="Devices Found", select_text="Select", none_text="Rescan")
if option:
sim800.btpair(option['id'])
passcode = sim800.btparingpasscode()
correct_passcode = prompt_boolean(passcode, title="Started connection from other device?", font=FONT_MEDIUM_BOLD)
if correct_passcode:
sim800.btpairconfirm() #TODO: 4 number passcodes?
return True
else:
sim800.btpairreject()
return False
else:
return False
def pairing_callback(param):
''' Callback for incoming pairing request '''
global g_paired
accept = prompt_boolean("Accept pairing request from %s"%param, title="Incoming pairing")
if accept:
sim800.btpairconfirm(0000)
# Check if we did pair
if len(sim800.btpaired()) > 1:
g_paired = True
else:
sim800.btpairreject()
def set_simple_pairing():
''' Set pairing mode to 4 digit pin, default 0000 '''
sim800.command("AT+BTPAIRCFG=1,0000", 1000, "OK") # TODO: Error checking?
#Initialise
ugfx_helper.init()
ugfx.init()
ugfx.clear()
ugfx.text(5,5, "Powering Up SIM800", ugfx.BLACK)
sim800.poweron()
ugfx.clear()
ugfx.text(5,5, "Enabling Bluetooth", ugfx.BLACK)
sim800.btpoweron()
sim800.btname(BLUETOOTH_NAME)
sim800.poweroff()
sim800.poweron()
sim800.btpoweron() # Needs a full cycle to have an effect
sim800.btvisible(True)
# Set pairing mode
set_simple_pairing()
ugfx.text(5,20, "Addr: %s " % sim800.btaddress(), ugfx.BLACK)
ugfx.text(5,35, "Name: %s " % sim800.btname(), ugfx.BLACK)
ugfx.clear()
# Register pairings callback
sim800.registercallback("+BTPAIRING:", pairing_callback)
clear_pairing = prompt_boolean("Delete all bluetooth pairings?",title="Clear Pairings?", true_text="Yes", false_text="No")
if clear_pairing:
sim800.btunpair(0) #0 = clear every pairing
# Start main loop
ugfx.clear()
ugfx.Label(5,5, 220, 200, "Connect to %s \n Passcode = 0000 \n Press menu to exit" % BLUETOOTH_NAME)
connected = True
while(True):
# Check for pairing button
if (buttons.is_triggered(buttons.Buttons.BTN_1)):
pairing_dialog()
# Check for exit button
if (buttons.is_triggered(buttons.Buttons.BTN_Menu)):
sim800.btpoweroff()
app.restart_to_default()
num_connections = len(sim800.btconnected())
if (connected == False) and (num_connections > 0): # Gained connection
ugfx.area(0,220,240,320, ugfx.BLACK) #Blank bottom of screen
print(sim800.btconnected())
sim800.speakervolume(100)
sim800.btvoicevolume(100)
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.text(5,230,"CONNECTED!", ugfx.GREEN)
ugfx.set_default_font(ugfx.FONT_SMALL)
connected = True
elif (connected == True) and (num_connections == 0): # Lost connection
ugfx.area(0,220,240,320, ugfx.BLACK) #Blank bottom of screen
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.text(5,230,"DISCONNECTED", ugfx.RED)
ugfx.set_default_font(ugfx.FONT_SMALL)
connected = False
sleep.wfi()

36
boot.py
View File

@ -1,9 +1,11 @@
import pyb, os, micropython, sys
import os, tilda
from machine import Neopix
micropython.alloc_emergency_exception_buf(100)
sys.path.append('/flash/upip')
n=Neopix()
n.display([0,0,0])
n.display([0,0,0])
print("EMF: boot.py")
os.sync()
root = os.listdir()
@ -12,7 +14,6 @@ def app(a):
return a + "/main.py"
def file(file, remove):
print(file)
try:
a = None
with open(file, 'r') as f:
@ -21,14 +22,25 @@ def file(file, remove):
os.remove(file)
return app(a)
except Exception as e:
print(e)
print("Not found: %s" % file)
def any_home():
return app(next(a for a in root if a.startswith("home")))
h = [a for a in root if a.startswith("home")]
return h[0] if len(h) else False
start = None
if "main.py" in root:
start = "main.py"
start = file("once.txt", True) or file("default_app.txt", False) or any_home() or "bootstrap.py"
if "no_boot" in root:
os.remove("no_boot")
print("no_boot found, aborting boot sequence")
elif "bootstrap.py" in root:
print("Bootstrapping...")
tilda.main("bootstrap.py")
else:
start = None
if "main.py" in root:
start = "main.py"
start = start or file("once.txt", True) or file("default_app.txt", False) or any_home()
if ".py" not in start:
start += "/main.py"
print("Booting into %s" % start)
tilda.main(start)
pyb.main(start)

199
bootstrap.py Normal file
View File

@ -0,0 +1,199 @@
"""Bootstraps the badge by downloading the base software"""
import ugfx, machine, network, json, time, usocket, os, gc
from tilda import Buttons
HOST = "badgeserver.emfcamp.org"
wifi = network.WLAN()
wifi.active(True)
# Helpers
def msg(text):
ugfx.clear()
ugfx.text(5, 5, "EMF 2018", ugfx.BLACK)
ugfx.text(5, 30, "TiLDA Mk4", ugfx.BLACK)
lines = text.split("\n")
print(lines[0])
for i, line in enumerate(lines):
ugfx.text(5, 65 + i * 20, line, ugfx.BLACK)
def wifi_select():
msg("Please select your wifi\nConfirm with button A")
sl = ugfx.List(5, 110, 228, 204)
aps = {}
while not Buttons.is_pressed(Buttons.BTN_A):
for s in (wifi.scan() or []):
if s[0] not in aps:
sl.add_item(s[0])
aps[s[0]] = s
time.sleep(0.01)
ugfx.poll()
ssid = sl.selected_text()
sl.destroy()
msg("Wifi: %s\nPlease enter your password\nConfirm with button A" % ssid)
kb = ugfx.Keyboard(0, 160, 240, 170)
e = ugfx.Textbox(5, 130, 228, 25, text="")
while not Buttons.is_pressed(Buttons.BTN_A):
time.sleep(0.01)
ugfx.poll()
pw = e.text()
e.destroy()
kb.destroy()
result = {"ssid":ssid,"pw":pw}
with open("wifi.json", "wt") as file:
file.write(json.dumps(result))
file.flush()
os.sync()
return result
def wifi_details():
try:
with open("wifi.json") as f:
return json.loads(f.read())
except Exception as e:
print(str(e))
return wifi_select()
def connect():
details = wifi_details()
if 'user' in details:
wifi.connect(details['ssid'], details['pw'], enterprise=True, entuser=details['user'], entmethod=wifi.EAP_METHOD_PEAP0_MSCHAPv2, entserverauth=False)
elif 'pw' in details:
wifi.connect(details['ssid'], details['pw'])
else:
wifi.connect(details['ssid'])
wait_until = time.ticks_ms() + 10000
while not wifi.isconnected():
if (time.ticks_ms() > wait_until):
os.remove("wifi.json")
raise OSError("Wifi timeout");
time.sleep(0.1)
def addrinfo(host, port, retries_left = 20):
try:
return usocket.getaddrinfo(host, port)[0][4]
except OSError as e:
if ("-15" in str(e)) and retries_left:
# [addrinfo error -15]
# This tends to happen after startup and goes away after a while
time.sleep_ms(200)
return addrinfo(host, port, retries_left - 1)
else:
raise e
def get(path):
s = usocket.socket()
s.connect(addrinfo(HOST, 80))
body = b""
status = None
try:
s.send('GET /2018/%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, HOST))
state = 1
hbuf = b""
clen = 9999999
headers = {}
while len(body) < clen:
buf = s.recv(1024)
if state == 1: # Status
nl = buf.find(b"\n")
if nl > -1:
hbuf += buf[:nl - 1]
status = int(hbuf.split(b' ')[1])
state = 2
hbuf = b"";
buf = buf[nl + 1:]
else:
hbuf += buf
if state == 2: # Headers
hbuf += buf
nl = hbuf.find(b"\n")
while nl > -1:
if nl < 2:
buf = hbuf[2:]
hbuf = None
state = 3
clen = int(headers["content-length"])
break
header = hbuf[:nl - 1].decode("utf8").split(':', 3)
headers[header[0].strip().lower()] = header[1].strip()
hbuf = hbuf[nl + 1:]
nl = hbuf.find(b"\n")
if state == 3: # Content
body += buf
finally:
s.close()
if status != 200:
raise Exception("HTTP %d for %s" % (status, path))
return body
# os.path bits
def split(path):
if path == "":
return ("", "")
r = path.rsplit("/", 1)
if len(r) == 1:
return ("", path)
head = r[0]
if not head:
head = "/"
return (head, r[1])
def dirname(path):
return split(path)[0]
def exists(path):
try:
os.stat(path)[0]
return True
except OSError:
return False
def makedirs(path):
sub_path = split(path)[0]
if sub_path and (not exists(sub_path)):
makedirs(sub_path)
if not exists(path):
os.mkdir(path)
# Steps
def step_wifi():
while not wifi.isconnected():
msg("Connecting to wifi...");
try:
connect()
except Exception as e:
print(str(e))
msg("Couldn't connect\nPlease check wifi details")
time.sleep(1)
def step_download():
msg("Connecting to server...")
files = list(json.loads(get("bootstrap")).keys())
for i, file in enumerate(files):
msg("Downloading - %d%%\n%s" % (100 * i // len(files), file))
makedirs(dirname(file))
with open(file, 'wb') as f:
f.write(get("download?repo=emfcamp/Mk4-Apps&path=%s" % file))
os.sync()
def step_goodbye():
msg("All done!\n\nRestarting badge...")
os.remove("bootstrap.py")
time.sleep(2)
machine.reset()
ugfx.init()
machine.Pin(machine.Pin.PWM_LCD_BLIGHT).on()
try:
step_wifi()
step_download()
step_goodbye()
except Exception as e:
msg("Error\nSomething went wrong :(\n\n" + str(e))
raise e

235
breakout/main.py Normal file
View File

@ -0,0 +1,235 @@
"""Breakout!"""
___title___ = "Breakout"
___license___ = "MIT"
___categories___ = ["Games"]
___dependencies___ = ["app", "ugfx_helper", "buttons"]
from tilda import Buttons
import ugfx, ugfx_helper, dialogs
import time
import app
import random
import math
background_colour = ugfx.BLACK
framerate = 60
SCREEN_WIDTH = 240
SCREEN_HEIGHT = 320
class Ball:
def __init__(self, x = 5.0, y = 5.0, dx = 2, dy = 2):
self.colour = ugfx.WHITE
self.diameter = 4
self.x = x
self.y = y
self.dy = dx
self.dx = dy
def centerX(self):
return self.x + self.diameter / 2
def centerY(self):
return self.y + self.diameter / 2
def left(self):
return self.x
def right(self):
return self.x + self.diameter
def top(self):
return self.y
def bottom(self):
return self.y + self.diameter
def draw(self):
ugfx.fill_ellipse(int(self.x), int(self.y), self.diameter, self.diameter, self.colour)
def clear(self):
ugfx.fill_ellipse(int(self.x), int(self.y), self.diameter, self.diameter, background_colour)
def bounceX(self):
self.dx *= -1
def bounceY(self):
self.dy *= -1
def bounceUpwards(self, ratioFromMiddle):
speed = math.sqrt(self.dx * self.dx + self.dy * self.dy)
self.dx = math.sin(ratioFromMiddle) * speed
self.dy = math.cos(ratioFromMiddle) * speed * -1
def tick(self):
self.x += self.dx
self.y += self.dy
if self.x < 0 or self.x + self.diameter > SCREEN_WIDTH:
self.bounceX()
if self.y < 0 or self.y + self.diameter > SCREEN_HEIGHT:
self.bounceY()
def hasCollidedWith(self, item):
return self.right() >= item.left() and self.left() <= item.right() and self.top() <= item.bottom() and self.bottom() >= item.top()
def isHorizontalCollision(self, item):
return self.centerY() >= item.top() and self.centerY() <= item.bottom()
def isVerticalCollision(self, item):
return self.centerX() >= item.left() and self.centerX() <= item.right()
def hasHitTop(self, item):
return self.y + self.diameter >= item.top()
def horizontalPositionFromMiddle(self, item):
return min(1, max(0, (self.centerX() - item.left()) / (item.right() - item.left()))) - 1
class Paddle:
def __init__(self, x = SCREEN_WIDTH / 2, width = SCREEN_WIDTH // 4, dx = 10):
self.x = x
self.dx = dx
self.width = width
self.height = 4
self.colour = ugfx.WHITE
def left(self):
return self.x - self.width / 2
def right(self):
return self.x + self.width / 2
def top(self):
return self.bottom() - self.height
def bottom(self):
return SCREEN_HEIGHT
def draw(self):
ugfx.area(int(self.left()), int(self.top()), self.width, self.height, self.colour)
def clear(self):
ugfx.area(int(self.left()), int(self.top()), self.width, self.height, background_colour)
def tick(self):
if Buttons.is_pressed(Buttons.JOY_Right) and self.right() < SCREEN_WIDTH:
self.x += self.dx
if Buttons.is_pressed(Buttons.JOY_Left) and self.left() > 0:
self.x -= self.dx
class Block:
def __init__(self, x, y, width, height, colour = ugfx.WHITE):
self.x = x
self.y = y
self.width = width
self.height = height
self.colour = colour
self.visible = True
def left(self):
return self.x
def right(self):
return self.x + self.width
def top(self):
return self.y
def bottom(self):
return self.y + self.height
def draw(self):
colour = self.colour if self.visible else background_colour
ugfx.area(int(self.left()), int(self.top()), self.width, self.height, colour)
def clear(self):
ugfx.area(int(self.left()), int(self.top()), self.width, self.height, background_colour)
def hide(self):
self.visible = False
self.clear()
# Clear LEDs
leds = Neopix()
leds.display([0,0,0])
leds.display([0,0,0])
ugfx_helper.init()
ugfx.clear(background_colour)
def randomColour():
return random.randint(0, 0xffffff)
def gameEnd(score):
ugfx.text(5, 5, str(score) + ' POINTS!!!', ugfx.WHITE)
for i in range(0, 10):
leds.display([randomColour(), 0])
time.sleep(0.1)
leds.display([0, randomColour()])
time.sleep(0.1)
leds.display([0, 0])
time.sleep(1)
def gameOver(score):
ugfx.text(5, 5, 'GAME OVER', ugfx.WHITE)
ugfx.text(5, 30, str(score) + ' points', ugfx.WHITE)
for i in range(0, 5):
leds.display([0xff0000, 0])
time.sleep(0.2)
leds.display([0, 0xff0000])
time.sleep(0.2)
leds.display([0, 0])
time.sleep(1)
def runGame():
paddle = Paddle()
direction = random.random() - 0.5
initial_speed_up = 4
ball = Ball(x = SCREEN_WIDTH / 2, y = SCREEN_HEIGHT / 2, dx = math.cos(direction) * initial_speed_up, dy = math.sin(direction) * initial_speed_up)
blocks = \
[Block(x = x, y = 30, width = 36, height = 10, colour = ugfx.RED) for x in range(24, SCREEN_WIDTH - 24, 40)] + \
[Block(x = x, y = 44, width = 36, height = 10, colour = ugfx.GREEN) for x in range(24, SCREEN_WIDTH - 24, 40)] + \
[Block(x = x, y = 58, width = 36, height = 10, colour = ugfx.BLUE) for x in range(24, SCREEN_WIDTH - 24, 40)] + \
[Block(x = x, y = 72, width = 36, height = 10, colour = ugfx.YELLOW) for x in range(24, SCREEN_WIDTH - 24, 40)] + \
[Block(x = x, y = 86, width = 36, height = 10, colour = ugfx.ORANGE) for x in range(24, SCREEN_WIDTH - 24, 40)]
def invisibleBlocks():
return [block for block in blocks if not(block.visible)]
for block in blocks:
block.draw()
while True:
paddle.draw()
ball.draw()
time.sleep(1.0 / framerate)
paddle.clear()
ball.clear()
paddle.tick()
ball.tick()
if Buttons.is_pressed(Buttons.BTN_Menu):
gameRunning = False
if all([not(block.visible) for block in blocks]):
gameEnd(score = 50 + len(invisibleBlocks()))
break
if ball.hasHitTop(paddle):
if ball.hasCollidedWith(paddle):
ball.bounceUpwards(ball.horizontalPositionFromMiddle(paddle))
else:
gameOver(score = len(invisibleBlocks()))
break
for block in blocks:
if block.visible and ball.hasCollidedWith(block):
block.hide()
if ball.isHorizontalCollision(block):
ball.bounceX()
if ball.isVerticalCollision(block):
ball.bounceY()
runGame()
app.restart_to_default()

76
btscan/main.py Normal file
View File

@ -0,0 +1,76 @@
"""Scan for and display nearby bluetooth devices"""
___title___ = "Bluetooth Scan"
___license___ = "MIT"
___dependencies___ = ["sleep", "app", "sim800"]
___categories___ = ["Other", "System"]
import ugfx, app
from machine import Neopix
np = Neopix()
import sim800
from tilda import Buttons
from time import sleep
btrestore = False
duration = 10
status_height = 20
ugfx.init()
ugfx.clear()
ugfx.set_default_font(ugfx.FONT_FIXED)
def instructions(duration):
ugfx.Label(5, 180, 240, 30, "Press A to start, B to change scan length or MENU to exit")
ugfx.Label(5, 210, 240, 15, "Scan requires ~{0} seconds".format(duration))
if not sim800.btison():
sim800.btpoweron()
btrestore = True
instructions(duration)
# while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
while not Buttons.is_pressed(Buttons.BTN_Menu):
a = Buttons.is_pressed(Buttons.BTN_A)
b = Buttons.is_pressed(Buttons.BTN_B)
if not a and not b:
ugfx.poll()
continue
if b:
duration = duration + 5
if duration > 60:
duration = 5
ugfx.clear()
instructions(duration)
continue
ugfx.clear()
np.display([0,0])
np.display([0x000099, 0x000099])
devs = sim800.btscan(duration*1000)
np.display([0x00, 0x00])
if len(devs) == 0:
ugfx.Label(0, 0, 240, 25, "No devices found")
np.display([0x110000,0x110000])
sleep(1)
np.display([0,0])
else:
if type(devs[0]) == int:
devs = [devs]
y = 0
for dev in devs[:20]:
ugfx.Label(0, y, 240, 25, "{3}dB {1}".format(*dev))
y += status_height
instructions(duration)
## App quitting...
if btrestore:
sim800.btpoweroff()
ugfx.clear()
app.restart_to_default()

4272
cards_against_emf/cards.json Normal file

File diff suppressed because it is too large Load Diff

58
cards_against_emf/main.py Normal file
View File

@ -0,0 +1,58 @@
''' Random card generator, includes Base Set, The First Expansion, The Second Expansion, The Third Expansion, The Fourth Expansion, The Fifth Expansion, The Sixth Expansion, Green Box Expansion, 90s Nostalgia Pack, Box Expansion, Fantasy Pack, Food Pack, Science Pack and World Wide Web Pack '''
___name___ = "Cards Against EMF"
___license___ = ["MIT"]
___dependencies___ = ["ugfx_helper", "sleep"]
___categories___ = ["Games"]
___bootstrapped___ = False # Whether or not apps get downloaded on first install. Defaults to "False", mostly likely you won't have to use this at all.
import ugfx, json, random
from tilda import Buttons
from app import restart_to_default
ugfx.init()
ugfx.clear()
ugfx.text(10, 10, "CARDS AGAINST EMF", ugfx.BLACK)
ugfx.text(10, 40, "A for a question", ugfx.BLACK)
ugfx.text(10, 60, "B for an answer", ugfx.BLACK)
ugfx.text(10, 80, "MENU to exit", ugfx.BLACK)
b=ugfx.Style()
b.set_background(ugfx.BLACK)
b.set_enabled([ugfx.WHITE, ugfx.BLACK, ugfx.BLACK, ugfx.BLACK]) # sets the style for when something is enabled
w=ugfx.Style()
w.set_background(ugfx.WHITE)
with open("cards_against_emf/cards.json") as data:
d = json.load(data)
def get_black():
x = random.randint(1, 320)
ugfx.clear(ugfx.html_color(0x000000))
text = str(d["blackCards"][x]["text"])
ugfx.Label(0, 0, 240, 400, text, style=b)
def get_white():
y = random.randint(1, 1271)
ugfx.clear(ugfx.html_color(0xffffff))
text = str(d["whiteCards"][y])
ugfx.Label(0, 0, 240, 400, text, style=w)
Buttons.enable_interrupt(
Buttons.BTN_A,
lambda button_id:get_black(),
on_press=True,
on_release=False)
Buttons.enable_interrupt(
Buttons.BTN_B,
lambda button_id:get_white(),
on_press=True,
on_release=False)
Buttons.enable_interrupt(
Buttons.BTN_Menu,
lambda button_id:restart_to_default(),
on_press=True,
on_release=False)

BIN
cmd.exe.lnk Normal file

Binary file not shown.

86
colourPicker/main.py Normal file
View File

@ -0,0 +1,86 @@
"""Colour picker to show on neopixels"""
___name___ = "ColourPicker"
___title___ = "Colour Picker"
___license___ = "MIT"
___dependencies___ = ["ugfx_helper"]
___categories___ = ["LEDs"]
import ugfx, ugfx_helper, app
from tilda import Buttons
from time import sleep
from machine import Neopix
def getColour(intensity, angle):
intensity *= 2
if angle < (1 / 6):
return (intensity, intensity * (angle * 6), 0) if intensity < 1 else (1, (angle * 6) + ((1 - (angle * 6)) * (intensity - 1)), (intensity - 1))
elif angle < (2 / 6):
return (intensity * (2 - (6 * angle)), intensity, 0) if intensity < 1 else ((2 - (6 * angle)) + ((1 - (2 - (6 * angle))) * (intensity - 1)), 1, (intensity - 1))
elif angle < (3 / 6):
return (0, intensity, intensity * ((6 * angle) - 2)) if intensity < 1 else ((intensity - 1), 1, ((6 * angle) - 2) + ((1 - ((6 * angle) - 2)) * (intensity - 1)))
elif angle < (4 / 6):
return (0, intensity * (4 - (6 * angle)), intensity) if intensity < 1 else ((intensity - 1), (4 - (6 * angle)) + ((1 - (4 - (6 * angle))) * (intensity - 1)), 1)
elif angle < (5 / 6):
return (intensity * ((6 * angle) - 4), 0, intensity) if intensity < 1 else (((6 * angle) - 4) + ((1 - ((6 * angle) - 4)) * (intensity - 1)), (intensity - 1), 1)
else:
return (intensity, 0, intensity * 6 * (1 - angle)) if intensity < 1 else (1, (intensity - 1), (6 * (1 - angle)) + ((1 - (6 * (1 - angle))) * (intensity - 1)))
ugfx_helper.init()
ugfx.clear()
maxHeight = ugfx.height()
n = Neopix()
# Draw colour swatch
for x in range(ugfx.width()):
intensity = x / ugfx.width()
for y in range(maxHeight):
(r, g, b) = getColour(intensity, y / ugfx.height())
colour = (int(31 * r) << 11) + (int(63 * g) << 5) + int(31 * b)
ugfx.area(x, y, 1, 1, colour)
i = 0
j = 0
ugfx.area((i - 1) if i > 0 else 0, (j - 1) if j > 0 else 0, 3 if (i > 0 and i < (ugfx.width() - 1)) else 2, 3 if (j > 0 and j < (maxHeight - 1)) else 2, ugfx.WHITE)
while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
changed = False
oldI = i
oldJ = j
if Buttons.is_pressed(Buttons.JOY_Right) and (i < (ugfx.width() - 1)):
i += 1
changed = True
elif Buttons.is_pressed(Buttons.JOY_Left) and (i > 0):
i -= 1
changed = True
if Buttons.is_pressed(Buttons.JOY_Down) and (j < (maxHeight - 1)):
j += 1
changed = True
elif Buttons.is_pressed(Buttons.JOY_Up) and (j > 0):
j -= 1
changed = True
if changed:
(r, g, b) = getColour(i / ugfx.width(), j / ugfx.height())
colour = (int(255 * r) << 16) + (int(255 * g) << 8) + int(255 * b)
n.display([colour, colour])
for xx in range((oldI - 1) if (oldI > 0) else 0, 1 + ((oldI + 1) if (oldI < (ugfx.width() - 2)) else (ugfx.width() - 1))):
intensity = xx / ugfx.width()
for yy in range((oldJ - 1) if (oldJ > 0) else 0, 1 + ((oldJ + 1) if (oldJ < (maxHeight - 2)) else (maxHeight - 1))):
(rr, gg, bb) = getColour(intensity, yy / ugfx.height())
colour = (int(31 * rr) << 11) + (int(63 * gg) << 5) + int(31 * bb)
ugfx.area(xx, yy, 1, 1, colour)
ugfx.area((i - 1) if i > 0 else 0, (j - 1) if j > 0 else 0, 3 if (i > 0 and i < (ugfx.width() - 1)) else 2, 3 if (j > 0 and j < (maxHeight - 1)) else 2, ugfx.WHITE)
sleep(0.05)
ugfx.clear()
app.restart_to_default()

124
custom_image_home/main.py Normal file
View File

@ -0,0 +1,124 @@
"""
Clone of the default homescreen for the Tilda Mk4.
Shows the EMF homescreen and a picture loaded on the badge alternately.
"""
___title___ = "Custom Image Home"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "shared/logo.png", "shared/sponsors.png"]
import ugfx
from homescreen import *
import time
import os
# We ❤️ our sponsors
ugfx.display_image(0, 0, "shared/sponsors.png")
wait = 5
while wait:
wait -= 1
sleep_or_exit(0.5)
def drawEMFscreen():
# Padding for name
intro_height = 30
intro_text = "Hi! I'm"
name_height = 60
status_height = 20
info_height = 30
logo_path = "shared/logo.png"
logo_height = 150
logo_width = 56
# Maximum length of name before downscaling
max_name = 8
# Background stuff
init()
ugfx.clear(ugfx.html_color(0x800080))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x800080), ugfx.html_color(0x800080), ugfx.html_color(0x800080)])
style.set_background(ugfx.html_color(0x800080))
ugfx.set_default_style(style)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text,
justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - info_height * 2, ugfx.width(), info_height, "TiLDA Mk4",
justification=ugfx.Label.CENTER)
# info
ugfx.Label(0, ugfx.height() - info_height, ugfx.width(), info_height, "Press MENU", justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - info_height * 2 - status_height, ugfx.width(), status_height, "",
justification=ugfx.Label.CENTER)
text = ""
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
def drawCustomImage():
ugfx.clear()
ugfx.orientation(90)
ugfx.display_image(0, 0, 'customImage.png')
def drawHelpText():
ugfx.clear()
ugfx. Label(0, 110, ugfx.width(), 100, "Copy an image named\ncustomImage.png with a\n240x320 resolution to the\nbadge root directory\nand it will appear!")
try:
f = open('customImage.png')
customImage = True
f.close()
except OSError:
customImage = False
# update loop
while True:
drawEMFscreen()
wait = 20
while wait:
wait -= 1
sleep_or_exit(0.5)
if customImage:
drawCustomImage()
else:
drawHelpText()
wait = 20
while wait:
wait -= 1
sleep_or_exit(0.5)

60
dowsingrod/main.py Normal file
View File

@ -0,0 +1,60 @@
"""This is a dowsing rod for WiFi APs"""
___title___ = "Dowsing Rod"
___license___ = "MIT"
___dependencies___ = ["sleep", "app", "wifi", "sim800"]
___categories___ = ["EMF", "System"]
import ugfx, wifi, app
from tilda import Buttons
from time import sleep
status_height = 20
ssid = 'emfcamp-legacy18'
ugfx.init()
ugfx.clear()
ugfx.set_default_font(ugfx.FONT_FIXED)
ugfx.Label(5, 180, 240, 15, "Press A to scan, MENU to exit")
# while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
while not Buttons.is_pressed(Buttons.BTN_Menu):
if not Buttons.is_pressed(Buttons.BTN_A) and not Buttons.is_pressed(Buttons.BTN_B):
ugfx.poll()
continue
if Buttons.is_pressed(Buttons.BTN_B):
ugfx.clear()
ugfx.Label(0, 0, 240, 25, "SSID:")
ssid_box = ugfx.Textbox(0, 25, 240, 25, text=ssid)
ugfx.Keyboard(0, ugfx.height()//2, ugfx.width(), ugfx.height()//2)
ssid_box.set_focus()
while not Buttons.is_pressed(Buttons.BTN_A):
ugfx.poll()
continue
ssid = ssid_box.text()
ugfx.clear()
wifi.nic().active(False)
wifi.nic().active(True)
# networks = [{ "ssid": ap[0], "mac": ap[1], "channel": ap[2], "signal": ap[3] } for ap in wifi.nic().scan()]
networks = sorted([net for net in wifi.nic().scan() if net[0] == ssid], key=lambda n: n[3], reverse=True)
aps = []
for ap in [(net[1], net[3]) for net in networks]:
if ap[0] not in [ap[0] for ap in aps]:
aps.append(ap)
y = 0
for ap in aps[:20]:
ugfx.Label(0, y, 240, 25, "{1}dB {0}".format(*ap))
y += status_height
if len(aps) == 0:
ugfx.Label(0, y, 240, 25, "No %s APs found" % ssid)
ugfx.clear()
app.restart_to_default()

102
emfcampqueer_home/main.py Normal file
View File

@ -0,0 +1,102 @@
"""
emfcampqueer theme by ganbariley
"""
___title___ = "EMFCamp Rainbow Homescreen"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen"]
___launchable___ = False
___bootstrapped___ = False
import ugfx
from homescreen import *
import time
from tilda import Buttons
from machine import Pin
from machine import Neopix
torch = Pin(Pin.GPIO_FET)
neo = Neopix()
# Padding for name
intro_height = 30
intro_text = "Hi! I'm"
name_height = 60
status_height = 20
info_height = 30
logo_path = "emfcampqueer_home/pridelogo.png"
logo_height = 150
logo_width = 56
# Maximum length of name before downscaling
max_name = 8
torch_on = False
# Background stuff
init()
ugfx.clear(ugfx.html_color(0x800080))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x800080), ugfx.html_color(0x800080), ugfx.html_color(0x800080)])
style.set_background(ugfx.html_color(0x800080))
ugfx.set_default_style(style)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - info_height * 2, ugfx.width(), info_height, "TiLDA Mk4", justification=ugfx.Label.CENTER)
# info
ugfx.Label(0, ugfx.height() - info_height, ugfx.width(), info_height, "Press MENU", justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - info_height * 2 - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
# update loop
while True:
text = "";
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
if Buttons.is_pressed(Buttons.BTN_Star):
if torch_on:
torch_on = False
torch.off()
neo.display([0,0])
else:
torch_on = True
torch.on()
neo.display([0xffffff,0xffffff])
sleep_or_exit(0.5)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

75
enby/main.py Normal file
View File

@ -0,0 +1,75 @@
"""enby flag homescreen
Similar to the default homescreen, but the
background is the enby flag. Based on Pride Flag Homescreen by marekventur
"""
___title___ = "Enby"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "app"]
from app import restart_to_default
import ugfx
import homescreen
homescreen.init()
ugfx.clear(ugfx.html_color(0xFF0000))
# Used for placement around text
name_height = 55
info_height = 20
# Maximum length of name before downscaling
max_name = 8
# Orientation for other people to see
ugfx.orientation(90)
# enby flag colours
colours = [0xfff433, 0xffffff, 0x9b59d0, 0x000000]
# Draw each "band" of colour in the flag
colour_width = ugfx.width() / len(colours)
for num, colour in enumerate(colours):
width_loc = int(num * colour_width)
ugfx.area(width_loc, 0, int(colour_width), 320, ugfx.html_color(colour))
ugfx.set_default_font(ugfx.FONT_NAME)
# Calc center of screen
center = (int(ugfx.width() / 2), int(ugfx.height() / 2))
# Process name
given_name = homescreen.name("Set your name in the settings app")
if len(given_name) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, given_name, justification=ugfx.Label.CENTER)
# Draw for the user to see
ugfx.orientation(270)
ugfx.set_default_font(ugfx.FONT_SMALL)
# WiFi/Battery update loop
while True:
ugfx.area(0, ugfx.height() - info_height, ugfx.width(), info_height, ugfx.WHITE)
wifi_strength_value = homescreen.wifi_strength()
if wifi_strength_value:
wifi_message = 'WiFi: %s%%' % int(wifi_strength_value)
wifi_text = ugfx.text(center[0], ugfx.height() - info_height, wifi_message, ugfx.BLACK)
battery_value = homescreen.battery()
if battery_value:
battery_message = 'Battery: %s%%' % int(battery_value)
battery_text = ugfx.text(0, ugfx.height() - info_height, battery_message, ugfx.BLACK)
homescreen.sleep_or_exit(1.5)
restart_to_default()

106
game-of-life/main.py Normal file
View File

@ -0,0 +1,106 @@
"""Game of Life"""
___title___ = "Conway game of life"
___license___ = "MIT"
___categories___ = ["Games"]
___dependencies___ = ["app", "ugfx_helper", "sleep", "buttons"]
import app, ugfx, ugfx_helper, buttons, sleep, time, random
from tilda import Buttons
# the game of life logic
class Board:
def __init__(self, width, height):
self.width = width
self.height = height
self.data = [random.randint(0,1) for x in range(width * height)]
def __str__(self):
res = "w: {} h: {}".format(self.width, self.height)
for j in range(0, self.height):
row = [self.value(i, j) for i in range(self.width)]
res = res + "\n" + row
return res
def value(self, x, y):
return self.data[x * self.width + y]
def neighbours(self, x, y):
neighbCoords = [(i, j)
for i in range(x - 1, x + 2) if i >= 0 and i < self.width
for j in range(y - 1, y + 2) if j >= 0 and j < self.height
]
return [self.value(neighbCoord[0], neighbCoord[1])
for neighbCoord in neighbCoords if neighbCoord != (x, y) ]
# returns the new value of a given cell
def nextValue(self, x, y):
neighbsArr = self.neighbours(x, y)
liveNeighbs = 0
for neighb in neighbsArr:
if (neighb):
liveNeighbs = liveNeighbs + 1
if(self.value(x, y)):
if (liveNeighbs <= 1):
return 0 # underpopulation
else:
if (liveNeighbs <= 3):
return 1 # lives
else:
return 0 # overpopulation
else:
if (liveNeighbs == 3):
return 1 # reproduction
else:
return 0 # dies
# update the board data in place
def step(self):
self.data = [self.nextValue(x, y) for x in range(self.width) for y in range(self.height)]
# now the displaying part
ugfx_helper.init()
ugfx.clear()
grid_size = 5
grid_width = round(ugfx.width() / grid_size)
grid_height = round(ugfx.height() / grid_size)
alive_colours = [ugfx.WHITE, ugfx.GRAY, ugfx.BLUE, ugfx.RED, ugfx.GREEN, ugfx.YELLOW, ugfx.ORANGE]
dead_colour = ugfx.BLACK
def displayCell(x, y, alive):
if(alive):
colour = alive_colours[random.randrange(len(alive_colours))]
else:
colour = dead_colour
ugfx.area(x*grid_size, y*grid_size, grid_size, grid_size, colour)
def displayBoard(board):
coords = [(x, y) for x in range(board.width) for y in range(board.height)]
for (x, y) in coords:
displayCell(x, y, board.value(x, y))
board = Board(grid_width, grid_height)
while True:
displayBoard(board)
board.step()
#time.sleep(1)
sleep.wfi()
if buttons.is_triggered(Buttons.BTN_Menu):
break
ugfx.clear()
app.restart_to_default()

27
hello_world/main.py Normal file
View File

@ -0,0 +1,27 @@
"""This is a simple hello world app"""
___title___ = "Hello World"
___license___ = "MIT"
___dependencies___ = ["sleep", "app"]
___categories___ = ["EMF"]
import ugfx, sleep, app
from tilda import Buttons
# initialize screen
ugfx.init()
ugfx.clear()
# show text
ugfx.text(5, 5, "Hello World!!", ugfx.BLACK)
# waiting until a button has been pressed
while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
sleep.wfi()
# closing
ugfx.clear()
app.restart_to_default()

BIN
holland/brenno.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
holland/eu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

266
holland/main.py Normal file
View File

@ -0,0 +1,266 @@
"""Camp Holland app"""
___title___ = "Holland"
___license___ = "MIT"
___dependencies___ = ["app", "sim800", "ugfx_helper"]
___categories___ = ["Villages"]
___bootstrapped___ = False
from app import *
from dialogs import *
import ugfx
import ugfx_helper
from machine import Neopix
def show_screen(color1, color2, text, text2="", flip=False):
if flip:
ugfx.orientation(90)
ugfx.clear(ugfx.html_color(color1))
ugfx.set_default_font(ugfx.FONT_NAME)
ugfx.text(0, 100, text, ugfx.html_color(color2))
ugfx.set_default_font(ugfx.FONT_SMALL)
ugfx.text(0, 200, text2, ugfx.html_color(color2))
if flip:
ugfx.orientation(270)
def show_vip(inv):
if (inv):
show_screen(0xFFFFFF, 0xFFA400, "Dutch VIP", "", True)
else:
show_screen(0xFFA400, 0xFFFFFF, "Dutch VIP", "", True)
def show_flag():
ugfx.display_image(0, 0, "holland/nederland.png")
def show_boot():
ugfx.display_image(0, 0, "holland/start.png")
ugfx_helper.init()
ugfx.clear()
show_boot()
import sim800
import time
from tilda import Buttons
sim800.poweron()
n = Neopix()
vip = False
vip_inv = False
strobe = False
def cbButtonA(button_id):
global vip
vip = False
show_flag()
def cbButtonB(button_id):
global vip
vip = True
show_vip(vip_inv)
def load():
global vip
vip = False
show_screen(0x000000, 0xFFFFFF, "LOADING")
print("Copy AMR")
sim800.fscreate("REC\\1.AMR")
f = open('holland/wilhelmus.amr', 'r')
data = f.read(256)
c = len(data)
sim800.fswrite("REC\\1.AMR", data, True)
pr = c
while (c>0):
data = f.read(256)
c = len(data)
sim800.fswrite("REC\\1.AMR", data, False)
pr = pr + c
show_screen(0x000000, 0xFFFFFF, "LOADING", str(pr))
print(str(pr))
f.close()
show_screen(0x000000, 0xFFFFFF, "DONE")
wilhelmus = (
("D", 300), ("G", 300), ("G", 300), ("A", 300), ("B", 300), ("C2", 300), ("A", 300), ("B", 300), ("A", 300), ("B", 300), ("C2", 300), ("B", 300), ("A", 300), ("G", 300), ("A", 600), ("G", 600), ("D", 300),
("G", 300), ("G", 300), ("A", 300), ("B", 300), ("C2", 300), ("A", 300), ("B", 300), ("A", 300), ("B", 300), ("C", 300), ("B", 300), ("A", 600), ("G", 600), ("A", 600), ("G", 600), ("B", 300), ("C", 300),
)
freq = {
"C": 2616,
"D": 2936,
"E": 3296,
"F": 3492,
"G": 3920,
"A": 4400,
"B": 4938,
"C2": 5322,
}
def cbButtonMenu(button_id):
restart_to_default()
def cbButtonCall(button_id):
sim800.speakervolume(100)
show_screen(0x000000, 0xFFFFFF, "TONE")
for note, length in wilhelmus:
sim800.playtone(freq.get(note, 9000), length, False)
def cbButton1(button_id):
global vip
vip = False
ugfx.display_image(0, 0, "holland/eu.png")
def cbButton2(button_id):
sim800.speakervolume(100)
sim800.stopplayback()
show_screen(0x000000, 0xFFFFFF, "PLAY")
a = sim800.startplayback(1,0,100)
if not a:
sim800.fsrm("REC\\1.AMR")
sim800.fsrm("REC\\2.AMR")
sim800.fsrm("REC\\3.AMR")
load()
show_screen(0x000000, 0xFFFFFF, "PLAY")
sim800.startplayback(1,0,100)
def cbButton3(button_id):
show_screen(0x000000, 0xFFFFFF, "STOP")
sim800.stopplayback()
def cbButton4(button_id):
global vip
vip = False
ugfx.display_image(0, 0, "holland/otter.png")
def cbButton5(button_id):
n.display([0xFFFFFF, 0xFFFFFF])
def cbButton6(button_id):
n.display([0x000000, 0x000000])
def cbButton7(button_id):
global vip
vip = False
show_boot()
def cbButton8(button_id):
global strobe
strobe = True
def cbButton9(button_id):
global strobe
strobe = False
def cbButtonHash(button_id):
global vip
vip = False
ugfx.display_image(0, 0, "holland/brenno.png")
Buttons.enable_interrupt(
Buttons.BTN_Menu,
cbButtonMenu,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_Call,
cbButtonCall,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_A,
cbButtonA,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_B,
cbButtonB,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_1,
cbButton1,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_2,
cbButton2,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_3,
cbButton3,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_4,
cbButton4,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_5,
cbButton5,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_6,
cbButton6,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_7,
cbButton7,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_8,
cbButton8,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_9,
cbButton9,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_Hash,
cbButtonHash,
on_press=True,
on_release=False);
vip = True
aaa = False
while True:
if vip_inv:
vip_inv = False
else:
vip_inv = True
if vip:
show_vip(vip_inv)
if strobe:
if aaa:
n.display([0xFFA500, 0xFFA500])
aaa = False
else:
n.display([0x000000, 0x000000])
aaa = True
if not vip:
time.sleep(0.1)
else:
time.sleep(0.1)

BIN
holland/nederland.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
holland/otter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
holland/start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
holland/wilhelmus.amr Normal file

Binary file not shown.

51
hologram_demo/main.py Normal file
View File

@ -0,0 +1,51 @@
"""This app connects to the Hologram service via GPRS displays recieved data on the screen and sets the neopixles"""
___title___ = "Hologram Demo"
___license___ = "MIT"
___dependencies___ = ["app", "sim800"]
___categories___ = ["EMF", "System"]
___bootstrapped___ = False
#import ugfx, os, time, sleep, app, sim800
import ugfx, app, sim800
import os
from tilda import Buttons
from time import sleep
from machine import Neopix
n = Neopix()
ugfx.init()
ugfx.clear()
ugfx.set_default_font(ugfx.FONT_FIXED)
def callback(data):
payload=data.decode("utf-8")
ugfx.Label(5, 100, 240, 15, payload)
colour = int(payload)
n.display([colour,colour])
print('Launching Hologram Demo')
ugfx.Label(5, 20, 240, 15, "Starting....")
sim800.setup_gprs()
ugfx.Label(5, 20, 240, 15, "GPRS Ready")
sim800.connect_gprs('hologram')
ugfx.Label(5, 40, 240, 15, "GPRS Connected")
sim800.start_server(4010, callback)
ugfx.Label(5, 60, 240, 15, "Server Started")
ugfx.Label(5, 300, 240, 15, "** Hold A or B or MENU to exit **")
while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
sleep(2)
ugfx.clear()
ugfx.Label(5, 20, 240, 15, "Stopping....")
sim800.stop_server()
sim800.stop_gprs()
app.restart_to_default()

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

125
home_aerospace/main.py Normal file
View File

@ -0,0 +1,125 @@
"""Default homescreen
Hackedup awful code for a london aerospace themed badge
"""
___name___ = "Aerospace Badge"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "wifi", "http", "ugfx_helper", "sleep"]
___launchable___ = False
import ugfx, random, time, wifi, http, math
from tilda import LED, Buttons
from machine import Neopix
from homescreen import *
import time
cycle = 0
#colourList = [0xff0000,0x00ff00]
colourList = [0xFF0000, 0xFFFFFF, 0x00FF00, 0x0000FF, 0xFFF000, 0xD800FF, 0xFF008F, 0x00FFF7]
n = Neopix()
# We ❤️ our sponsors
ugfx.display_image(0, 0, "home_aerospace/aerospace-logo.png")
wait = 5
while wait:
wait-=1
sleep_or_exit(0.5)
def ledChange():
colourNum1 = colourList[random.randint(0,len(colourList)-1)]
colourNum2 = colourList[random.randint(0,len(colourList)-1)]
while colourNum1 == colourNum2:
colourNum2 = colourList[random.randint(0,len(colourList)-1)]
n.display([colourNum1,colourNum2])
# Padding for name
intro_height = 30
intro_text = "London Aerospace"
intro_width = 200
intro_position_left = 0
name_height = 60
status_height = 30
info_height = 30
tick = 0
logo_path = "home_aerospace/aerospace-logo.png"
logo_height = 250
logo_width = 250
aerospace_text = "London Aerospace Yo"
# Maximum length of name before downscaling
max_name = 8
# Background stuff
init()
ugfx.clear(ugfx.html_color(0xFFFFFF))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.BLACK, ugfx.html_color(0xFFFFFF), ugfx.html_color(0xFFFFFF), ugfx.html_color(0xFFFFFF)])
style.set_background(ugfx.html_color(0xFFFFFF))
ugfx.set_default_style(style)
# Draw for people to see
ugfx.orientation(90)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2 - 20),
logo_path
)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
intro_object = ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
# info
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - 30, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
status.text('BATTERY INCOMING')
# update loop
while True:
text = "";
if math.fmod(tick, 100) == 0:
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
tick +=1
# if intro_position_left > -intro_width:
# intro_position_left -= 1
# intro_object.x(
# intro_position_left
# )
# else:
# intro_object.x(0)
# intro_position_left = 0
ledChange()
sleep_or_exit(0.05)

View File

@ -5,16 +5,94 @@ It gets automatically installed when a badge is
newly activated or reset.
"""
___name___ = "Homescreen (Default)"
___license___ = "GPL"
___categories___ = ["homescreen"]
___title___ = "Homescreen (Default)"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "shared/logo.png", "shared/sponsors.png"]
___launchable___ = False
___bootstrapped___ = True
print("there")
import ugfx, homescreen
import ugfx
from homescreen import *
import time
from tilda import Buttons
homescreen.init(color = 0xe4ffdb)
# We ❤️ our sponsors
init()
ugfx.display_image(0, 0, "shared/sponsors.png")
wait_until = time.ticks_ms() + 3000
while time.ticks_ms() < wait_until:
time.sleep(0.1)
if Buttons.is_pressed(Buttons.BTN_A) or Buttons.is_pressed(Buttons.BTN_B) or Buttons.is_pressed(Buttons.BTN_Menu):
break
ugfx.display_image(0, 0, "home_default/bg.gif")
ugfx.text(20, 20, homescreen.name(), ugfx.BLACK)
# Padding for name
intro_height = 30
intro_text = "Hi! I'm"
name_height = 60
status_height = 20
info_height = 30
logo_path = "shared/logo.png"
logo_height = 150
logo_width = 56
# Maximum length of name before downscaling
max_name = 8
# Background stuff
ugfx.clear(ugfx.html_color(0x800080))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x800080), ugfx.html_color(0x800080), ugfx.html_color(0x800080)])
style.set_background(ugfx.html_color(0x800080))
ugfx.set_default_style(style)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - info_height * 2, ugfx.width(), info_height, "TiLDA Mk4", justification=ugfx.Label.CENTER)
# info
ugfx.Label(0, ugfx.height() - info_height, ugfx.width(), info_height, "Long Press MENU", justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - info_height * 2 - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
# update loop
while True:
text = "";
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
sleep_or_exit(0.5)

BIN
home_ham/emf_ham.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

90
home_ham/main.py Normal file
View File

@ -0,0 +1,90 @@
"""Amateur Radio homescreen
This is a modified version of the default homescreen that allows you to set a callsign
"""
___title___ = "Amateur Radio Homescreen"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen"]
import ugfx
from homescreen import *
import time
from tilda import Buttons
init()
# Padding for name
intro_height = 30
name_height = 60
status_height = 20
callsign_height = 50
info_height = 30
logo_path = "home_ham/emf_ham.png"
logo_width = 200
# Maximum length of name before downscaling
max_name = 8
# Background stuff
ugfx.clear(ugfx.html_color(0xffffff))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.BLACK, ugfx.html_color(0xffffff), ugfx.html_color(0xffffff), ugfx.html_color(0xffffff)])
style.set_background(ugfx.html_color(0xffffff))
ugfx.set_default_style(style)
ugfx.orientation(90)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
30,
logo_path
)
# Draw for people to see
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
# Process name
name_setting = name("Set your name in the settings app")
callsign_setting = callsign("Set your callsign in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, 220 ,ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Title
if len(callsign_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw callsign
ugfx.Label(0, 270, ugfx.width(), callsign_height, callsign_setting, justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
# Draw for wearer to see
ugfx.orientation(270)
status = ugfx.Label(0, 300, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
# update loop
while True:
text = "";
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
sleep_or_exit(0.5)
app.restart_to_default()

93
home_pycon/main.py Normal file
View File

@ -0,0 +1,93 @@
"""PyCon homescreen
This is the default homescreen for the Tilda Mk4.
It gets automatically installed when a badge is
newly activated or reset.
"""
___title___ = "Homescreen (PyCon)"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen"]
___launchable___ = False
___bootstrapped___ = False
import ugfx
from homescreen import *
import time
from tilda import Buttons
init()
# Padding for name
intro_height = 30
intro_text = "Hi! I'm"
name_height = 60
status_height = 20
info_height = 30
logo_path = "home_pycon/python_single.png"
logo_height = 82
logo_width = 55
# Maximum length of name before downscaling
max_name = 8
# Background stuff
bg_color = 0xfecb2f
ugfx.clear(ugfx.html_color(bg_color))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.BLACK, ugfx.html_color(bg_color), ugfx.html_color(bg_color), ugfx.html_color(bg_color)])
style.set_background(ugfx.html_color(bg_color))
ugfx.set_default_style(style)
# Draw for people to see
ugfx.orientation(90)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - info_height * 2, ugfx.width(), info_height, "TiLDA Mk4", justification=ugfx.Label.CENTER)
# info
ugfx.Label(0, ugfx.height() - info_height, ugfx.width(), info_height, "Long Press MENU", justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - info_height * 2 - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
# update loop
while True:
text = "";
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
sleep_or_exit(0.5)

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

BIN
home_stratum0/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

88
home_stratum0/main.py Normal file
View File

@ -0,0 +1,88 @@
"""Stratum 0 homescreen
This is the Stratum 0 flavored homescreen for the Tilda Mk4.
"""
___title___ = "Homescreen (Stratum 0)"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen"]
import ugfx
from homescreen import *
import time
from tilda import Buttons
# Init Homescreen
init()
# Padding for name
intro_height = 30
intro_text = "Moin! I'm"
name_height = 60
status_height = 20
info_height = 30
logo_path = "home_stratum0/logo.png"
logo_height = 106
logo_width = 58
# Maximum length of name before downscaling
max_name = 8
# Background stuff
ugfx.clear(ugfx.html_color(0x000000))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x000000), ugfx.html_color(0x000000), ugfx.html_color(0x000000)])
style.set_background(ugfx.html_color(0x000000))
ugfx.set_default_style(style)
# Logo stuff
ugfx.orientation(90)
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - info_height * 2, ugfx.width(), info_height, "TiLDA Mk4", justification=ugfx.Label.CENTER)
# info
ugfx.Label(0, ugfx.height() - info_height, ugfx.width(), info_height, "Long Press MENU", justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - info_height * 2 - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
# WiFi/Battery update loop
while True:
text = "";
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
sleep_or_exit(0.5)
app.restart_to_default()

BIN
home_trans/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

175
home_trans/main.py Normal file
View File

@ -0,0 +1,175 @@
"""Trans homescreen
A version of the home screen that has a trans flag.
Press 0 to go back to normal or 8 to show the flag.
Hold * to activate all LEDs for use as a torch.
"""
___title___ = "Homescreen (Trans)"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "shared/logo.png"]
___launchable___ = False
___bootstrapped___ = False
import ugfx
from homescreen import *
import time
from tilda import Buttons
from machine import Pin
from machine import Neopix
torch = Pin(Pin.GPIO_FET)
neo = Neopix()
init()
# Padding for name
intro_height = 30
intro_text = "Hi! I'm"
name_height = 64
status_height = 20
info_height = 30
logo_path = "shared/logo.png"
trans_logo_path = "home_trans/logo.png"
logo_height = 150
logo_width = 56
# Maximum length of name before downscaling
max_name = 8
torch_on = False
# Background stuff
ugfx.clear(ugfx.html_color(0x55cdfc))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.BLACK, ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc)])
style.set_background(ugfx.html_color(0x55cdfc))
ugfx.set_default_style(style)
ugfx.display_image(0, 0, "home_trans/trans.png")
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2)+9,
trans_logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
style.set_enabled([ugfx.BLACK, ugfx.html_color(0xf8b0be), ugfx.html_color(0xf8b0be), ugfx.html_color(0xf8b0be)])
style.set_background(ugfx.html_color(0xf8b0be))
ugfx.set_default_style(style)
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Prepare to draw name
style.set_enabled([ugfx.BLACK, ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc)])
style.set_background(ugfx.html_color(0x55cdfc))
ugfx.set_default_style(style)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.LEFT)
def draw_badge():
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x800080), ugfx.html_color(0x800080), ugfx.html_color(0x800080)])
style.set_background(ugfx.html_color(0x800080))
ugfx.clear(ugfx.html_color(0x800080))
ugfx.set_default_style(style)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.LEFT)
def draw_trans():
style.set_enabled([ugfx.BLACK, ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc)])
style.set_background(ugfx.html_color(0x55cdfc))
ugfx.set_default_style(style)
ugfx.display_image(0, 0, "home_trans/trans.png")
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2)+9,
trans_logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
style.set_enabled([ugfx.BLACK, ugfx.html_color(0xf8b0be), ugfx.html_color(0xf8b0be), ugfx.html_color(0xf8b0be)])
style.set_background(ugfx.html_color(0xf8b0be))
ugfx.set_default_style(style)
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Prepare to draw name
style.set_enabled([ugfx.BLACK, ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc)])
style.set_background(ugfx.html_color(0x55cdfc))
ugfx.set_default_style(style)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.LEFT)
# update loop
while True:
text = "";
value_battery = battery()
if value_battery:
text += "%s%%" % int(value_battery)
if Buttons.is_pressed(Buttons.BTN_Star):
if torch_on:
torch_on = False
torch.off()
neo.display([0,0])
else:
torch_on = True
torch.on()
neo.display([0xffffff,0xffffff])
if Buttons.is_pressed(Buttons.BTN_8):
draw_trans()
if Buttons.is_pressed(Buttons.BTN_0):
draw_badge()
status.text(text)
sleep_or_exit(0.5)

BIN
home_trans/trans.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,25 +1,23 @@
"""Launcher for apps currently installed"""
___name___ = "Launcher"
___title___ = "Launcher"
___license___ = "MIT"
___categories___ = ["System"]
___categories___ = ["System", "Launcher"]
___dependencies___ = ["dialogs", "app", "ugfx_helper"]
___launchable___ = False
___bootstrapped___ = True
import ugfx
import ugfx_helper, ugfx
from app import *
from dialogs import *
ugfx_helper.init()
ugfx.clear()
apps = range(1, 10)
cols = 4
options = [{"title": a.title, "app": a} for a in get_apps()]
option = prompt_option(options, none_text="Home", text="Select App to start")
for i, p in enumerate(apps):
logical_x = i % cols
logical_y = i // cols
x = logical_x * width
print("launcher")
if not option:
restart_to_default()
else:
option["app"].boot()

View File

@ -7,6 +7,7 @@ ___license___ = "MIT"
___dependencies___ = ["metadata_reader", "ospath"]
from ospath import *
import os, machine
from metadata_reader import read_metadata
class App:
@ -57,6 +58,10 @@ class App:
return self.attributes[attribute]
return default
def boot(self):
write_launch_file(self.name)
machine.reset()
def __str__(self):
return self.title
@ -88,6 +93,10 @@ def get_apps(category=None):
return [app for app in _apps if app.matches_category(category)]
return _apps
def uncache_apps():
global _apps
_apps = None
_categories = None
def get_categories():
global _categories
@ -97,3 +106,13 @@ def get_categories():
_categories.update(app.categories)
return _categories
def write_launch_file(app, file = "once.txt"):
with open(file, "wt") as file:
file.write(app)
file.flush()
os.sync()
def restart_to_default():
write_launch_file("")
machine.reset()

View File

@ -8,33 +8,37 @@ from http import *
import hashlib, binascii
class BadgeStore:
def __init__(self, url = "http://badge.marekventur.com", repo="emfcamp/Mk4-Apps", ref="master"):
def __init__(self, url = "http://badgeserver.emfcamp.org/2018", repo="emfcamp/Mk4-Apps", ref="master"):
self.url = url
self.repo = repo
self.ref = ref
self._apps = None
def get_apps(self):
def get_all_apps(self):
if not self._apps:
self._apps = self._call("apps")
return self._apps
def get_apps(self, category):
return self.get_all_apps()[category]
def get_categories(self):
return self.get_apps().keys()
return self.get_all_apps().keys()
def get_app(self, app):
return self._call("app", {"app": app})
def get_prs(self):
return self._call("prs")
def install(self, apps):
files = self._call("install", {"apps": ",".join(apps)})
installers = []
url = "%s/download" % (self.url)
for path, hash in files.items():
if self._is_file_up_to_date(path, hash):
continue
params = {"repo": self.repo, "ref": self.ref, "path": path}
installers.append(Installer(path, url, params))
return installers
return self._create_installers(self._call("install", {"apps": ",".join(apps)}))
def update(self, apps):
return self._create_installers(self._call("update", {"apps": ",".join(apps)}))
def bootstrap(self):
return self._create_installers(self._call("bootstrap"))
def _call(self, command, params = {}):
params["repo"] = self.repo
@ -42,28 +46,61 @@ class BadgeStore:
with get("%s/%s" % (self.url, command), params=params).raise_for_status() as response:
return response.json() # todo: error handling
def _is_file_up_to_date(self, path, hash):
if not isfile(path):
return False
def _create_installers(self, files):
installers = []
url = "%s/download" % (self.url)
for path, hash in files.items():
if hash == get_hash(path):
continue
params = {"repo": self.repo, "ref": self.ref, "path": path}
installers.append(Installer(path, url, params, hash))
return installers
with open(path, "rb") as file:
sha256 = hashlib.sha256()
buf = file.read(128)
while len(buf) > 0:
sha256.update(buf)
buf = file.read(128)
current = str(binascii.hexlify(sha256.digest()), "utf8")[:10]
return current == hash
def _is_file_up_to_date(self, path, hash):
return hash == _get_hash(path)
TEMP_FILE = ".tmp.download"
class Installer:
def __init__(self, path, url, params):
def __init__(self, path, url, params, hash):
self.path = path
self.url = url
self.params = params
self.hash = hash
def download(self):
with get(self.url, params=self.params).raise_for_status() as response:
response.download(path)
count = 0
while get_hash(TEMP_FILE) != self.hash:
count += 1
if count > 5:
try:
os.remove(TEMP_FILE)
except:
pass
raise OSError("Aborting download of %s after 5 unsuccessful attempts" % self.path)
try:
get(self.url, params=self.params).raise_for_status().download_to(TEMP_FILE)
except OSError as e:
if "404" in str(e):
raise e
pass
try:
os.remove(self.path)
except OSError:
pass
makedirs(dirname(self.path))
os.rename(TEMP_FILE, self.path)
def get_hash(path):
if not isfile(path):
return None
with open(path, "rb") as file:
sha256 = hashlib.sha256()
buf = file.read(128)
while len(buf) > 0:
sha256.update(buf)
buf = file.read(128)
return str(binascii.hexlify(sha256.digest()), "utf8")[:10]

View File

@ -1,54 +1,24 @@
"""Convenience methods for dealing with the TiLDA buttons"""
"""Convenience methods for dealing with the TiLDA buttons
Pins are decined in tilda.Buttons.BTN_XYZ:
BTN_0 - BTN_9, BTN_Hash, BTN_Star
BTN_A, BTN_B
BTN_Call, BTN_End
BTN_Menu
JOY_Center, JOY_Down, JOY_Left, JOY_Right, JOY_Up
"""
___license___ = "MIT"
import pyb
import machine, time, tilda
CONFIG = {
"JOY_UP": pyb.Pin.PULL_DOWN,
"JOY_DOWN": pyb.Pin.PULL_DOWN,
"JOY_RIGHT": pyb.Pin.PULL_DOWN,
"JOY_LEFT": pyb.Pin.PULL_DOWN,
"JOY_CENTER": pyb.Pin.PULL_DOWN,
"BTN_MENU": pyb.Pin.PULL_UP,
"BTN_A": pyb.Pin.PULL_UP,
"BTN_B": pyb.Pin.PULL_UP
}
# Convenience
Buttons = tilda.Buttons
ROTATION_MAP = {
"JOY_UP": "JOY_LEFT",
"JOY_LEFT": "JOY_DOWN",
"JOY_DOWN": "JOY_RIGHT",
"JOY_RIGHT": "JOY_UP",
}
_tilda_pins = {}
_tilda_interrupts = {}
_tilda_bounce = {}
def _get_pin(button):
if button not in _tilda_pins:
raise ValueError("Please call button.init() first before using any other button functions")
return _tilda_pins[button]
def init(buttons = CONFIG.keys()):
"""Inits all pins used by the TiLDA badge"""
global _tilda_pins
for button in buttons:
_tilda_pins[button] = pyb.Pin(button, pyb.Pin.IN)
_tilda_pins[button].init(pyb.Pin.IN, CONFIG[button])
def rotate(button):
"""remaps names of buttons to rotated values"""
return ROTATION_MAP[button]
def is_pressed(button):
pin = _get_pin(button)
if pin.pull() == pyb.Pin.PULL_DOWN:
return pin.value() > 0
else:
return pin.value() == 0
return tilda.Buttons.is_pressed(button)
def is_triggered(button, interval = 30):
"""Use this function if you want buttons as a trigger for something in a loop
@ -59,31 +29,40 @@ def is_triggered(button, interval = 30):
global _tilda_bounce
if is_pressed(button):
if button in _tilda_bounce:
if pyb.millis() > _tilda_bounce[button]:
if time.ticks_ms() > _tilda_bounce[button]:
del _tilda_bounce[button]
else:
return False # The button might have bounced back to high
# Wait for a while to avoid bounces to low
pyb.delay(interval)
time.sleep_ms(interval)
# Wait until button is released again
while is_pressed(button):
pyb.wfi()
time.sleep_ms(1)
_tilda_bounce[button] = pyb.millis() + interval
_tilda_bounce[button] = time.ticks_ms() + interval
return True
# The following functions might not work
def has_interrupt(button):
global _tilda_interrupts
_get_pin(button)
if button in _tilda_interrupts:
return True
else:
return False
return False;
# todo: re-enable
#global _tilda_interrupts
#_get_pin(button)
#if button in _tilda_interrupts:
# return True
#else:
# return False
def enable_interrupt(button, interrupt, on_press = True, on_release = False):
raise Exception("interrupts don't work yet")
"""Attaches an interrupt to a button
on_press defines whether it should be called when the button is pressed
@ -105,35 +84,30 @@ def enable_interrupt(button, interrupt, on_press = True, on_release = False):
mode = None;
if on_press and on_release:
mode = pyb.ExtInt.IRQ_RISING_FALLING
mode = machine.ExtInt.IRQ_RISING_FALLING
else:
if pin.pull() == pyb.Pin.PULL_DOWN:
mode = pyb.ExtInt.IRQ_RISING if on_press else pyb.ExtInt.IRQ_FALLING
if pin.pull() == machine.Pin.PULL_DOWN:
mode = machine.ExtInt.IRQ_RISING if on_press else machine.ExtInt.IRQ_FALLING
else:
mode = pyb.ExtInt.IRQ_FALLING if on_press else pyb.ExtInt.IRQ_RISING
mode = machine.ExtInt.IRQ_FALLING if on_press else machine.ExtInt.IRQ_RISING
_tilda_interrupts[button] = {
"interrupt": pyb.ExtInt(pin, mode, pin.pull(), interrupt),
"interrupt": machine.ExtInt(pin, mode, pin.pull(), interrupt),
"mode": mode,
"pin": pin
}
def disable_interrupt(button):
raise Exception("interrupts don't work yet")
global _tilda_interrupts
if button in _tilda_interrupts:
interrupt = _tilda_interrupts[button]
pyb.ExtInt(interrupt["pin"], interrupt["mode"], interrupt["pin"].pull(), None)
machine.ExtInt(interrupt["pin"], interrupt["mode"], interrupt["pin"].pull(), None)
del _tilda_interrupts[button]
init([button])
def disable_all_interrupt():
raise Exception("interrupts don't work yet")
for interrupt in _tilda_interrupts:
disable_interrupt(interrupt)
def enable_menu_reset():
import onboard
enable_interrupt("BTN_MENU", lambda t:onboard.semihard_reset(), on_release = True)
def disable_menu_reset():
disable_interrupt("BTN_MENU")

View File

@ -6,7 +6,7 @@ Values can be anything json can store, including a dict
Usage:
import database
with database.open() as db:
with database.Database() as db:
print(db.get("hello", "default"))
db.set("foo", "world")
db.delete("bar")

View File

@ -1,11 +1,11 @@
"""Some basic UGFX powered dialogs"""
___license___ = "MIT"
___dependencies___ = ["buttons"]
___dependencies___ = ["buttons", "sleep"]
import ugfx
import buttons
import pyb
import ugfx, buttons, sleep
from buttons import Buttons
import time
default_style_badge = ugfx.Style()
default_style_badge.set_focus(ugfx.RED)
@ -18,11 +18,13 @@ default_style_dialog.set_background(ugfx.html_color(0xFFFFFF))
TILDA_COLOR = ugfx.html_color(0x7c1143);
FONT_SMALL = 0 #todo: find correct values
FONT_MEDIUM_BOLD = 0
def notice(text, title="TiLDA", close_text="Close", width = 260, height = 180, font=ugfx.FONT_SMALL, style=None):
prompt_boolean(text, title = title, true_text = close_text, false_text = None, width = width, height = height, font=font, style=style)
def notice(text, title="TiLDA", close_text="Close", font=FONT_SMALL, style=None):
prompt_boolean(text, title = title, true_text = close_text, false_text = None, font=font, style=style)
def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width = 260, height = 180, font=ugfx.FONT_SMALL, style=None):
def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", font=FONT_SMALL, style=None):
"""A simple one and two-options dialog
if 'false_text' is set to None only one button is displayed.
@ -31,35 +33,59 @@ def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width
global default_style_dialog
if style == None:
style = default_style_dialog
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
window = ugfx.Container((ugfx.width() - width) // 2, (ugfx.height() - height) // 2, width, height, style=style)
ugfx.set_default_font(FONT_MEDIUM_BOLD)
width = ugfx.width() - 10
height = ugfx.height() - 10
window = ugfx.Container(5, 5, width, height)
window.show()
ugfx.set_default_font(font)
window.text(5, 10, title, TILDA_COLOR)
window.line(0, 30, width, 30, ugfx.BLACK)
window.text(5, 5, title, TILDA_COLOR)
window.line(0, 25, width, 25, ugfx.BLACK)
if false_text:
true_text = "A: " + true_text
false_text = "B: " + false_text
ugfx.set_default_font(font)
label = ugfx.Label(5, 30, width - 10, height - 80, text = text, parent=window)
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
button_yes = ugfx.Button(5, height - 40, width // 2 - 15 if false_text else width - 15, 30 , true_text, parent=window)
button_no = ugfx.Button(width // 2 + 5, height - 40, width // 2 - 15, 30 , false_text, parent=window) if false_text else None
label = ugfx.Label(5, 30, width - 10, height - 80, text = text, parent=window, justification=4)
ugfx.set_default_font(FONT_MEDIUM_BOLD)
button_yes = ugfx.Button(5, height - 40, width // 2 - 10 if false_text else width - 15, 30 , true_text, parent=window)
button_no = ugfx.Button(width // 2, height - 40, width // 2 - 10, 30 , false_text, parent=window) if false_text else None
# Find newlines in label text to scroll.
def find_all(a_str, sub):
start = 0
while True:
start = a_str.find(sub, start)
if start == -1: return
yield start + 1 # Trap: \n becomes a single character, not 2.
start += len(sub) # use start += 1 to find overlapping matches
new_line_pos = [0] + list(find_all(text, '\n'))
text_scroll_offset = 0
try:
buttons.init()
button_yes.attach_input(ugfx.BTN_A,0)
if button_no: button_no.attach_input(ugfx.BTN_B,0)
#button_yes.attach_input(ugfx.BTN_A,0) # todo: re-enable once working
#if button_no: button_no.attach_input(ugfx.BTN_B,0)
window.show()
while True:
pyb.wfi()
if buttons.is_triggered("BTN_A"): return True
if buttons.is_triggered("BTN_B"): return False
sleep.wfi()
if buttons.is_triggered(buttons.Buttons.BTN_A): return True
if buttons.is_triggered(buttons.Buttons.BTN_B): return False
# Allow scrolling by new lines.
if buttons.is_triggered(buttons.Buttons.JOY_Down):
if text_scroll_offset < len(new_line_pos)-1:
text_scroll_offset = text_scroll_offset + 1
label.text(text[new_line_pos[text_scroll_offset]:])
if buttons.is_triggered(buttons.Buttons.JOY_Up):
if (text_scroll_offset > 0):
text_scroll_offset=text_scroll_offset - 1
label.text(text[new_line_pos[text_scroll_offset]:])
finally:
window.hide()
@ -68,44 +94,37 @@ def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width
if button_no: button_no.destroy()
label.destroy()
def prompt_text(description, init_text = "", true_text="OK", false_text="Back", width = 300, height = 200, font=ugfx.FONT_MEDIUM_BOLD, style=default_style_badge):
def prompt_text(description, init_text="", true_text="OK", false_text="Back", font=FONT_MEDIUM_BOLD, style=default_style_badge, numeric=False):
"""Shows a dialog and keyboard that allows the user to input/change a string
Returns None if user aborts with button B
"""
window = ugfx.Container(int((ugfx.width()-width)/2), int((ugfx.height()-height)/2), width, height, style=style)
window = ugfx.Container(0, 0, ugfx.width(), ugfx.height())
if false_text:
true_text = "M: " + true_text
true_text = "A: " + true_text
false_text = "B: " + false_text
if buttons.has_interrupt("BTN_MENU"):
buttons.disable_interrupt("BTN_MENU")
ugfx.set_default_font(ugfx.FONT_MEDIUM)
kb = ugfx.Keyboard(0, int(height/2), width, int(height/2), parent=window)
edit = ugfx.Textbox(5, int(height/2)-30, int(width*4/5)-10, 25, text = init_text, parent=window)
ugfx.set_default_font(ugfx.FONT_SMALL)
button_yes = ugfx.Button(int(width*4/5), int(height/2)-30, int(width*1/5)-3, 25 , true_text, parent=window)
button_no = ugfx.Button(int(width*4/5), int(height/2)-30-30, int(width/5)-3, 25 , false_text, parent=window) if false_text else None
ugfx.set_default_font(FONT_MEDIUM_BOLD)
kb = ugfx.Keyboard(0, ugfx.height()//2, ugfx.width(), ugfx.height()//2, parent=window)
edit = ugfx.Textbox(2, ugfx.height()//2-60, ugfx.width()-7, 25, text = init_text, parent=window)
ugfx.set_default_font(FONT_SMALL)
button_yes = ugfx.Button(2, ugfx.height()//2-30, ugfx.width()//2-6, 25 , true_text, parent=window)
button_no = ugfx.Button(ugfx.width()//2+2, ugfx.height()//2-30, ugfx.width()//2-6, 25 , false_text, parent=window) if false_text else None
ugfx.set_default_font(font)
label = ugfx.Label(int(width/10), int(height/10), int(width*4/5), int(height*2/5)-60, description, parent=window)
label = ugfx.Label(ugfx.width()//10, ugfx.height()//10, ugfx.width()*4//5, ugfx.height()*2//5-90, description, parent=window)
try:
buttons.init()
button_yes.attach_input(ugfx.BTN_MENU,0)
if button_no: button_no.attach_input(ugfx.BTN_B,0)
window.show()
edit.set_focus()
# edit.set_focus() todo: do we need this?
while True:
pyb.wfi()
sleep.wfi()
ugfx.poll()
#if buttons.is_triggered("BTN_A"): return edit.text()
if buttons.is_triggered("BTN_B"): return None
if buttons.is_triggered("BTN_MENU"): return edit.text()
if buttons.is_triggered(buttons.Buttons.BTN_A): return edit.text()
if buttons.is_triggered(buttons.Buttons.BTN_B): return None
if buttons.is_triggered(buttons.Buttons.BTN_Menu): return edit.text()
handle_keypad(edit, numeric)
finally:
window.hide()
@ -117,50 +136,148 @@ def prompt_text(description, init_text = "", true_text="OK", false_text="Back",
edit.destroy();
return
def prompt_option(options, index=0, text = "Please select one of the following:", title=None, select_text="OK", none_text=None):
last_key = None
last_keytime = None
def handle_keypad(edit, numeric):
global last_key, last_keytime
threshold = 1000
keymap = {
buttons.Buttons.BTN_0: [" ", "0"],
buttons.Buttons.BTN_1: ["1"],
buttons.Buttons.BTN_2: ["a", "b", "c", "2"],
buttons.Buttons.BTN_3: ["d", "e", "f", "3"],
buttons.Buttons.BTN_4: ["g", "h", "i", "4"],
buttons.Buttons.BTN_5: ["j", "k", "l", "5"],
buttons.Buttons.BTN_6: ["m", "n", "o", "6"],
buttons.Buttons.BTN_7: ["p", "q", "r", "s", "7"],
buttons.Buttons.BTN_8: ["t", "u", "v", "8"],
buttons.Buttons.BTN_9: ["w", "x", "y", "z", "9"],
buttons.Buttons.BTN_Hash: ["#"],
buttons.Buttons.BTN_Star: ["*", "+"],
}
for key, chars in keymap.items():
if buttons.is_triggered(key):
if numeric:
edit.text(edit.text() + chars[-1])
elif key != last_key:
edit.text(edit.text() + chars[0])
else:
if last_keytime is None or (time.ticks_ms() - last_keytime) > threshold:
edit.text(edit.text() + chars[0])
else:
last_char = edit.text()[-1]
try:
last_index = chars.index(last_char)
except ValueError:
# not sure how we get here...
return
next_index = (last_index+1) % len(chars)
edit.text(edit.text()[:-1] + chars[next_index])
last_key = key
last_keytime = time.ticks_ms()
def prompt_option(options, index=0, text = None, title=None, select_text="OK", none_text=None):
"""Shows a dialog prompting for one of multiple options
If none_text is specified the user can use the B or Menu button to skip the selection
if title is specified a blue title will be displayed about the text
"""
ugfx.set_default_font(ugfx.FONT_SMALL)
ugfx.set_default_font(FONT_SMALL)
window = ugfx.Container(5, 5, ugfx.width() - 10, ugfx.height() - 10)
window.show()
list_y = 30
if title:
window.text(5, 10, title, TILDA_COLOR)
window.text(5, 5, title, TILDA_COLOR)
window.line(0, 25, ugfx.width() - 10, 25, ugfx.BLACK)
window.text(5, 30, text, ugfx.BLACK)
list_y = 50
list_y = 30
if text:
list_y += 20
window.text(5, 30, text, ugfx.BLACK)
else:
window.text(5, 10, text, ugfx.BLACK)
options_list = ugfx.List(5, list_y, ugfx.width() - 25, 180 - list_y, parent = window)
options_list = ugfx.List(5, list_y, ugfx.width() - 24, 265 - list_y, parent = window)
options_list.disable_draw()
optnum = 1
for option in options:
if isinstance(option, dict) and option["title"]:
options_list.add_item(option["title"])
title = option["title"]
else:
options_list.add_item(str(option))
title = str(option)
if optnum < 11:
# mod 10 to make 10th item numbered 0
options_list.add_item("{}: {}".format((optnum % 10),title))
else:
options_list.add_item(" {}".format(title))
optnum = optnum + 1
options_list.enable_draw()
options_list.selected_index(index)
select_text = "A: " + select_text
if none_text:
none_text = "B: " + none_text
button_select = ugfx.Button(5, ugfx.height() - 50, 140 if none_text else ugfx.width() - 25, 30 , select_text, parent=window)
button_none = ugfx.Button(ugfx.width() - 160, ugfx.height() - 50, 140, 30 , none_text, parent=window) if none_text else None
button_select = ugfx.Button(5, ugfx.height() - 50, 105 if none_text else 200, 30 , select_text, parent=window)
button_none = ugfx.Button(116, ugfx.height() - 50, 105, 30 , none_text, parent=window) if none_text else None
try:
buttons.init()
while True:
pyb.wfi()
sleep.wfi()
ugfx.poll()
if buttons.is_triggered("BTN_A"): return options[options_list.selected_index()]
if button_none and buttons.is_triggered("BTN_B"): return None
if button_none and buttons.is_triggered("BTN_MENU"): return None
# todo: temporary hack
#if (buttons.is_triggered(buttons.Buttons.JOY_Up)):
# index = max(index - 1, 0)
# options_list.selected_index(index)
#if (buttons.is_triggered(buttons.Buttons.JOY_Down)):
# index = min(index + 1, len(options) - 1)
# options_list.selected_index(index)
if buttons.is_triggered(buttons.Buttons.BTN_A) or buttons.is_triggered(buttons.Buttons.JOY_Center):
return options[options_list.selected_index()]
if button_none and buttons.is_triggered(buttons.Buttons.BTN_B): return None
if button_none and buttons.is_triggered(buttons.Buttons.BTN_Menu): return None
# These are indexes for selected_index, 1 means "First item", ie index 0. 0 is treated as if it were 10
button_nums = {
Buttons.BTN_1: 0,
Buttons.BTN_2: 1,
Buttons.BTN_3: 2,
Buttons.BTN_4: 3,
Buttons.BTN_5: 4,
Buttons.BTN_6: 5,
Buttons.BTN_7: 6,
Buttons.BTN_8: 7,
Buttons.BTN_9: 8,
Buttons.BTN_0: 9,
}
for key, num in button_nums.items():
if buttons.is_triggered(key):
# No need to check for too large an index; gwinListSetSelected validates this.
options_list.selected_index(num)
break
if buttons.is_triggered(Buttons.BTN_Hash):
# Page down
idx = options_list.selected_index() + 10
cnt = options_list.count()
if idx >= cnt:
idx = cnt - 1
options_list.selected_index(idx)
continue
if buttons.is_triggered(Buttons.BTN_Star):
# Page up
idx = options_list.selected_index() - 10
if idx < 0:
idx = 0
options_list.selected_index(idx)
continue
finally:
window.hide()
@ -172,23 +289,24 @@ def prompt_option(options, index=0, text = "Please select one of the following:"
class WaitingMessage:
"""Shows a dialog with a certain message that can not be dismissed by the user"""
def __init__(self, text = "Please Wait...", title="TiLDA"):
def __init__(self, text="Please Wait...", title="TiLDA"):
self.window = ugfx.Container(30, 30, ugfx.width() - 60, ugfx.height() - 60)
self.window.show()
self.window.text(5, 10, title, TILDA_COLOR)
self.window.line(0, 30, ugfx.width() - 60, 30, ugfx.BLACK)
self.label = ugfx.Label(5, 40, self.window.width() - 10, ugfx.height() - 40, text = text, parent=self.window)
self.window.text(5, 5, title, TILDA_COLOR)
self.window.line(0, 25, ugfx.width() - 60, 25, ugfx.BLACK)
self.label = ugfx.Label(5, 40, self.window.width() - 15, ugfx.height() - 40, text = text, parent=self.window)
# Indicator to show something is going on
self.indicator = ugfx.Label(ugfx.width() - 100, 0, 20, 20, text = "...", parent=self.window)
self.timer = pyb.Timer(3)
self.timer.init(freq=3)
self.timer.callback(lambda t: self.indicator.visible(not self.indicator.visible()))
#self.indicator = ugfx.Label(ugfx.width() - 100, 0, 20, 20, text = "...", parent=self.window)
#self.timer = machine.Timer(3)
#self.timer.init(freq=3)
#self.timer.callback(lambda t: self.indicator.visible(not self.indicator.visible()))
# todo: enable this once we have a timer somewhere
def destroy(self):
self.timer.deinit()
#self.timer.deinit()
self.label.destroy()
self.indicator.destroy()
#self.indicator.destroy()
self.window.destroy()
@property

16
lib/hall_effect.py Normal file
View File

@ -0,0 +1,16 @@
"""Library for sleep related functions"""
import machine
_adc = None
def hall_effect_adc():
global _adc
if not _adc:
_adc = machine.ADC(machine.ADC.ADC_HALLEFFECT)
return _adc
def get_flux():
return hall_effect_adc().convert() # todo: convert this into something meaningful

View File

@ -6,7 +6,7 @@ In particular, they *should*:
* Call "homescreen.init()" at the beginning. This will initiate ugfx, clear the screen and
initiate button handline.
* Use "pyb.wfi()" as much as possible to avoid draining the battery.
* Use "sleep.wfi()" as much as possible to avoid draining the battery.
* Not use
They also *may*:
@ -16,29 +16,64 @@ They also *may*:
* Display remaining battery "homescreen.battery()" (0-1)
"""
__license___ = "MIT"
__dependencies___ = ["database", "buttons"]
___license___ = "MIT"
___dependencies___ = ["database", "buttons", "app", "sleep", "ugfx_helper", "wifi", "sim800"]
import database, ugfx, buttons
import database, ugfx, random, buttons, tilda, sleep, ugfx_helper, wifi, time, sim800
from app import App
def init(color = 0xFFFFFF):
ugfx.init()
ugfx.clear(ugfx.html_color(color))
buttons.init()
#buttons.enable_interrupt()
_state = None
def init(enable_menu_button = True):
global _state
_state = {"menu": False}
ugfx_helper.init()
def menu():
ugfx.clear()
if enable_menu_button:
pass
#buttons.enable_interrupt("BTN_MENU", lambda t: set_state("menu"), on_release = True)
def name():
return database.get("homescreen.name", "bar")
def set_state(key, value = True):
# we can't allocate memory in interrupts, so make sure all keys are set beforehand and
# you're only using numbers and booleans
global _state
_state[key] = value
def mobile_strength():
return 0.75
def clean_up():
pass
def time_as_string(seconds=False):
t = time.localtime()
if seconds:
return "%d:%02d:%02d" % (t[3], t[4], t[5])
return "%d:%02d" % (t[3], t[4]) #todo: add a setting for AM/PM mode
def sleep_or_exit(interval = 0.5):
# todo: do this better - check button multiple times and sleep for only a short while
if buttons.is_triggered(tilda.Buttons.BTN_Menu):
clean_up()
launcher = "launcher"
try:
with open("default_launcher.txt", "r") as dl:
launcher=dl.readline()
except OSError:
pass
App(launcher).boot()
sleep.sleep(interval)
def name(default = None):
return database.get("homescreen.name", default)
def callsign(default = None):
return database.get("homescreen.callsign", default)
# Strength in %, None if unavailable
def wifi_strength():
return 0.65
return wifi.get_strength()
# Charge in %, None if unavailable
def battery():
return 0.65
return sim800.batterycharge() # todo: fix me, we can get this from the sim800

View File

@ -4,13 +4,12 @@ Somewhat inspired by "request".
Current known issues:
* HTTPS is not supported
*
"""
___license___ = "MIT"
___dependencies___ = ["urlencode"]
___dependencies___ = ["urlencode", "wifi"]
import usocket, ujson, os, time, gc, wifi
import usocket, ujson, os, time, gc, wifi, ussl
from urlencode import urlencode
"""Usage
@ -76,7 +75,11 @@ class Response(object):
self.socket = None
def json(self):
return ujson.loads(self.text)
try:
return ujson.loads(self.text)
except ValueError as e:
print("Invalid JSON: %s" % self.text)
raise(e)
# Writes content into a file. This function will write while receiving, which avoids
# having to load all content into memory
@ -144,7 +147,6 @@ def open_http_socket(method, url, json=None, timeout=None, headers=None, data=No
if proto == 'http:':
port = 80
elif proto == 'https:':
raise OSError("HTTPS is currently not supported")
port = 443
else:
raise OSError('Unsupported protocol: %s' % proto[:-1])
@ -167,24 +169,17 @@ def open_http_socket(method, url, json=None, timeout=None, headers=None, data=No
content = None
# ToDo: Handle IPv6 addresses
if is_ipv4_address(host):
addr = (host, port)
else:
ai = usocket.getaddrinfo(host, port)
addr = ai[0][4]
addr = get_address_info(host, port)
sock = None
if proto == 'https:':
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM, usocket.SEC_SOCKET)
else:
sock = usocket.socket()
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
if params:
urlpath += "?" + urlencode(params)
sock.connect(addr)
if proto == 'https:':
sock.settimeout(0) # Actually make timeouts working properly with ssl
sock = ussl.wrap_socket(sock, ca_certs="DST Root CA X3", cert_reqs=ussl.CERT_OPTIONAL)
sock.send('%s /%s HTTP/1.0\r\nHost: %s\r\n' % (method, urlpath, host))
@ -202,6 +197,18 @@ def open_http_socket(method, url, json=None, timeout=None, headers=None, data=No
return sock
def get_address_info(host, port, retries_left = 20):
try:
return usocket.getaddrinfo(host, port)[0][4]
except OSError as e:
if ("-15" in str(e)) and retries_left:
# [addrinfo error -15]
# This tends to happen after startup and goes away after a while
time.sleep_ms(200)
return get_address_info(host, port, retries_left - 1)
else:
raise e
# Adapted from upip
def request(method, url, json=None, timeout=None, headers=None, data=None, params=None):
sock = open_http_socket(method, url, json, timeout, headers, data, params)
@ -258,5 +265,3 @@ def is_ipv4_address(address):
return len(valid_octets) == 4
except Exception:
return False

86
lib/ntp.py Normal file
View File

@ -0,0 +1,86 @@
"""Functions to sync the hardware clock to internet network time servers
Derived from the 2016 implementation.
"""
___license___ = "MIT"
import database
import usocket
import machine
# (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60
NTP_DELTA = 3155673600
# With Mk3 Firmware an IP address string works 5%, hangs at socket.socket(..) 95%, could be a bug in 2016 upython?
NTP_HOSTS = ["0.emfbadge.pool.ntp.org", "1.emfbadge.pool.ntp.org", "2.emfbadge.pool.ntp.org", "3.emfbadge.pool.ntp.org"]
NTP_PORT = 123
def get_NTP_time():
for NTP_HOST in NTP_HOSTS:
res = query_NTP_host(NTP_HOST)
if res is not None:
return res
return None
def query_NTP_host(_NTP_HOST):
NTP_QUERY = bytearray(48)
NTP_QUERY[0] = 0x1b
# Catch exception when run on a network without working DNS
try:
addr = usocket.getaddrinfo(_NTP_HOST, NTP_PORT)[0][-1]
except OSError:
return None
s = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
s.sendto(NTP_QUERY, addr)
# Setting timeout for receiving data. Because we're using UDP,
# there's no need for a timeout on send.
msg = None
try:
msg = s.recv(48)
except OSError:
pass
finally:
s.close()
if msg is None:
return None
import struct
stratum = int(msg[1])
if stratum == 0:
# KoD, reason doesn't matter, failover to next host
return None
val = struct.unpack("!I", msg[40:44])[0]
print("Using NTP Host: %s, Stratum: %d" % (_NTP_HOST, stratum))
return val - NTP_DELTA
def set_NTP_time():
import time
print("Setting time from NTP")
t = get_NTP_time()
if t is None:
print("Could not set time from NTP")
return False
tz = 0
with database.Database() as db:
tz = db.get("timezone", 0)
tz_minutes = int(abs(tz) % 100) * (1 if tz >= 0 else -1)
tz_hours = int(tz / 100)
t += (tz_hours * 3600) + (tz_minutes * 60)
tm = time.localtime(t)
tm = tm[0:3] + tm[3:6]
rtc = machine.RTC()
rtc.init(tm)
return True

View File

@ -10,13 +10,17 @@ import os
sep = "/"
R_OK = const(4)
W_OK = const(2)
X_OK = const(1)
F_OK = const(0)
R_OK = 4
W_OK = 2
X_OK = 1
F_OK = 0
def join(*args):
# TODO: this is non-compliant
if not args:
return ""
if len(args) == 1:
return args[0]
if type(args[0]) is bytes:
return b"/".join(args)
else:
@ -61,3 +65,25 @@ def isfile(path):
return stat.S_ISREG(mode)
except OSError:
return False
# not normally in os.path
def makedirs(path):
"""recursively creates a given path"""
sub_path = dirname(path)
if sub_path and (not exists(sub_path)):
makedirs(sub_path)
if not exists(path):
os.mkdir(path)
def recursive_rmdir(path=""):
for s in os.listdir(path):
full = join(path, s)
if isdir(full):
try:
recursive_rmdir(full)
except:
pass
os.rmdir(full)
else:
os.remove(full)

897
lib/sim800.py Normal file
View File

@ -0,0 +1,897 @@
"""SIM800 library for the TiLDA MK4"""
___license___ = "MIT"
___dependencies___ = []
import machine
import time
import micropython
import tilda
uart_port = 1
uart_default_baud = 115200
uart_timeout = 28
default_response_timeout = 2000
status_pin = machine.Pin(machine.Pin.GPIO_SIM_STATUS, machine.Pin.IN)
ringer_pin = machine.Pin(machine.Pin.GPIO_SIM_RI, machine.Pin.IN)
pwr_key_pin = machine.Pin(machine.Pin.GPIO_SIM_PWR_KEY, machine.Pin.OUT)
amp_pin = machine.Pin(machine.Pin.GPIO_AMP_SHUTDOWN, machine.Pin.OUT)
netlight_pin = machine.Pin(machine.Pin.GPIO_SIM_NETLIGHT, machine.Pin.IN)
# Open the UART
uart = machine.UART(uart_port, uart_default_baud, mode=machine.UART.BINARY, timeout=uart_timeout)
dirtybuffer = False # Flag if the buffer could have residual end of reresponsesponces line in it?
# A list of callback functions
callbacks = []
server_callback = None
# Globals for remembering callback data
clip = ""
btpairing = ''
holdoffirq=False
# Check if the SIM800 is powered up
def ison():
return status_pin.value()==1
# Check if the SIM800 is ringing
def isringing():
return ringer_pin.value()==0
# Register for a callback
def registercallback(call, function):
callbacks.append([call, function])
# Deregitser for a callback
def deregistercallback(function):
for entry in callbacks:
if entry[1]==function:
callbacks.remove(entry)
# Identify if this was a positive response
def ispositive(response):
return (response=="OK") or response.startswith("CONNECT") or response.startswith("SEND OK")
# Identify if this was a negative response
def isnegative(response):
return (response=="NO CARRIER") or (response=="ERROR") or (response=="NO DIALTONE") or (response=="BUSY") or (response=="NO ANSWER") or (response=="SEND FAIL") or (response=="TIMEOUT") or (response=="TimeOut")
# Identify if this is the completion of a response
def isdefinitive(response, custom=None):
if custom is not None:
return ispositive(response) or isnegative(response) or response.startswith(custom)
else:
return ispositive(response) or isnegative(response)
# Extract the [first/only] parameter from a response
def extractval(parameter, response, default=""):
for entry in response:
if entry.startswith(parameter):
return (entry[len(parameter):]).strip()
return default
# Extract all parameter from a response
def extractvals(parameter, response):
result = []
for entry in response:
if entry.startswith(parameter):
result.append((entry[len(parameter):]).strip())
return result
# Read a lines of response from the UART
def readline():
stringin = ""
while (True):
charin = uart.read(1)
# If we time out we are at the end of a line
if charin is None:
return stringin
# We this the end of the line?
elif (charin == b'\r'):
if (stringin!=""):
return stringin
# This will be part of the string then
elif not (charin == b'\n'):
stringin += chr(ord(charin))
# Check if we have a callback hook for this line
def processcallbacks(line):
global clip
global btpairing
# Check for the caller line information
if line.startswith("+CLIP:"):
clip = line[6:].strip()
# Check for Bluetooth pairing request
if line.startswith("+BTPAIRING:"):
btpairing = line[11:].strip()
# Handle TCP Server Data
if line.startswith("+RECEIVE"):
dlen = int(line.split(",")[2].rstrip(":"))+1
payload = uart.read(dlen)
if server_callback:
micropython.schedule(server_callback, payload[1:])
# Check for app callbacks
for entry in callbacks:
if line.startswith(entry[0]):
micropython.schedule(entry[1], line[len(entry[0]):].strip())
# Process the buffer for unsolicited result codes
def processbuffer():
while uart.any()>0:
line = readline()
processcallbacks(line)
# Execute a command on the module
# The same interface as and called by command() but without power so can be called from power()
def command_internal(command="AT", response_timeout=default_response_timeout, required_response=None, custom_endofdata=None):
global dirtybuffer
global holdoffirq
# Don't let the interupt process the buffer mid command
holdoffirq = True
# Process anything remaining in the buffer
processbuffer()
# Send the command
uart.write(command + "\r")
# Read the results
result = []
complete = False
customcomplete = required_response is None
timeouttime = time.time()+(response_timeout/1000)
while (time.time()<timeouttime):
line = readline()
processcallbacks(line)
# Remember the line if not empty
if (len(line)>0):
result.append(line)
# Check if we have a standard end of response
if isdefinitive(line, custom_endofdata):
complete = True
# Check if we have the data we are looking for
if (required_response is not None) and (line.startswith(required_response)):
customcomplete = True
# Check if we are done
if complete and customcomplete:
holdoffirq = False
return result
# We ran out of time
# set the dirty buffer flag is an out of date end of responcs cound end up in the buffer
if required_response is None:
dirtybuffer = True
result.append("TIMEOUT")
holdoffirq = False
return result
# Send the command to set the default configuration to the SIM800
def senddefaultconfig():
# Send a command to autonigotiate UART speed
command_internal("AT")
# Turn on new SMS notificationn
command_internal("AT+CNMI=1,2")
# Turn on calling line identification notification
command_internal("AT+CLIP=1")
# Swith to text mode
command("AT+CMGF=1")
# Switch to ASCII(ish)
command("AT+CSCS=\"8859-1\"")
# Enable DTMF detection
command("AT+DDET=1,0,1")
# Set up multichannel Bluetooth without transparent transmition
command("AT+BTSPPCFG=\"MC\",1")
command("AT+BTSPPCFG=\"TT\",0")
# Power on the SIM800 (True=on, False=off, returns true when on)
def power(onoroff, asyncro):
# Get to a stable state if not async
if not asyncro and pwr_key_pin.value():
pwr_key_pin.off()
time.sleep(3)
# Press the virtual power key if we are off
if not (ison()==onoroff):
pwr_key_pin.on()
if not asyncro:
time.sleep(3)
# Have we just turned on?
isonnow = ison()
if (isonnow==onoroff) and pwr_key_pin.value():
# Stop pressing the virtual key
pwr_key_pin.off()
# Clear the buffer
processbuffer()
dirtybuffer = False
# We are now live
if isonnow:
# Set the deault configuration
senddefaultconfig()
return isonnow
# Power on the SIM800 (returns true when on)
def poweron(asyncro=False):
return power(True, asyncro)
# Power off the SIM800 (returns true when off)
def poweroff(asyncro=False):
return not power(False, asyncro)
# Change the speed on the communication
def uartspeed(newbaud):
global uart
command("AT+IPR=" + str(newbaud))
uart.deinit()
if (newbaud==0):
uart = machine.UART(uart_port, uart_default_baud, mode=UART.BINARY, timeout=uart_timeout)
else:
uart = machine.UART(uart_port, newbaud, mode=UART.BINARY, timeout=uart_timeout)
# Netlight sheduled (called for polling uart)
def netlightscheduled_internal(pinstate):
# Complete the setup procedure if needed
if pwr_key_pin.value() and ison():
poweron()
# Check for incomming commands
if holdoffirq==False:
processbuffer()
# Netlight IRQ (called for polling uart)
def netlightirq_internal(pinstate):
micropython.schedule(netlightscheduled_internal, pinstate)
# Command is the AT command without the AT or CR/LF, response_timeout (in ms) is how long to wait for completion, required_response is to wait for a non standard response, custom_endofdata will finish when found
def command(command="AT", response_timeout=default_response_timeout, required_response=None, custom_endofdata=None):
# Check we are powered on and set up
poweron(False)
# Call the internal command() function
return command_internal(command, response_timeout, required_response, custom_endofdata)
# Make a voice call
def call(number):
command("ATD" + str(number) + ";", 20000)
# Answer a voice call
def answer():
command("ATA", 20000)
# End/reject a voice call
def hangup():
command("ATH")
# Redial the last number
def redial():
command("ATDL")
# Get the current/latest number to call
def latestnumber():
if ison():
processbuffer()
return clip.split(",")[0].strip("\"")
# Play DTMF tone(s) on a call
def dtmf(number):
validdigits = '1234567890#*ABCD'
for digit in str(number).upper():
if (digit in validdigits):
command("AT+VTS=" + digit)
elif (digit==','):
time.sleep(1)
# Send an SMS message
def sendsms(number, message):
# Swith to text mode
command("AT+CMGF=1")
# Switch to ASCII(ish)
command("AT+CSCS=\"8859-1\"")
# Send the message
command("AT+CMGS=\"" + str(number) + "\"", 2000, None, "> ")
return command(message + "\x1a", 60000)
# List the summery of SMS messages (0=unread,1=read,2=saved unread,3=saved sent, 4=all)
def listsms(stat=0):
statvals = ["REC UNREAD", "REC READ", "STO UNSENT", "STO SENT", "ALL"]
# Swith to text mode
command("AT+CMGF=1")
# Retrieve the list
return extractvals("+CMGL:", command("AT+CMGL=\"" + statvals[stat] + "\",1", 8000))
# Check if we have recived a new unread SMS message
def newsms():
return len(listsms(stat=0))>0
# Read an SMS message
def readsms(index, leaveunread=False):
# Swith to text mode
command("AT+CMGF=1")
# Switch to ASCII(ish)
command("AT+CSCS=\"8859-1\"")
# Retrieve the message
response = command("AT+CMGR=" + str(index) + "," + str(int(leaveunread)), 5000)
if (len(response)>=3):
return response[-2]
else:
return ""
# Delete an SMS message
def deletesms(index):
command("AT+CMGD=" + str(index), 5000)
# Get the IMEI number of the SIM800
def imei():
response = command("AT+GSN")
if (len(response)>=2):
return response[-2]
else:
return ""
# Get the IMSI number of the Sim Card
def imsi():
response = command("AT+CIMI")
if (len(response)>=2):
return response[-2]
else:
return ""
# Get the ICCID of the Sim Card
def iccid():
response = command("AT+ICCID")
return extractval("+ICCID:", response)
# Get the received signal strength indication
def rssi():
response = command("AT+CSQ")
return int(extractval("+CSQ:", response, "0,0").split(",")[0])
# Get the bit error rate
def ber():
response = command("AT+CSQ")
return int(extractval("+CSQ:", response, "0,0").split(",")[1])
# Get the cell engineering information (True to include neighboring cell id)
def engineeringinfo(neighbor=False):
command("AT+CENG=1," + str(int(neighbor)))
response = command("AT+CENG?")
command("AT+CENG=0")
responselist = extractvals("+CENG:", response)[1:]
results = []
for entry in responselist:
results.append([entry[0], entry.replace("\"", "").split(",")[1:]])
return results
# Get the cell id of the currently connected cell
def cellid():
return engineeringinfo()[0][1][6]
# Get/Set ringer volume (0-100)
def ringervolume(level=None):
# Set the new leve if we have one to set
if level is not None:
command("AT+CRSL=" + str(level))
# Retieve the set level to report back
response = command("AT+CRSL?")
return int(extractval("+CRSL:", response, 0))
# Get/Set speaker volume (0-100)
def speakervolume(level=None):
# Set the new leve if we have one to set
if level is not None:
command("AT+CLVL=" + str(level))
# Retieve the set level to report back
response = command("AT+CLVL?")
return int(extractval("+CLVL:", response, 0))
# Get/Set/Preview and set the ringtone (alert is 0-19)
def ringtone(alert=None,preview=False):
# Set/preview the new ringtone if we have one to set
if alert is not None:
command("AT+CALS=" + str(alert) + "," + str(int(preview)))
# Retieve the current/new setting
response = command("AT+CALS?")
current = extractval("+CALS:", response, 0).split(",")[0]
# Stop the preview unless we started it
if alert is None:
command("AT+CALS=" + current + ",0")
# Return the surrent setting
return int(current)
# Play a tone though the SIM800 (MHz and ms)
def playtone(freq=0,duration=2000,asyncro=True):
if freq>0:
command("AT+SIMTONE=1," + str(freq) + "," + str(duration) + ",0," + str(duration))
if not asyncro:
time.sleep(duration/1000)
else:
command("AT+SIMTONE=0")
# Record audio (id=1-10)
def startrecording(id=1, length=None):
if length is None:
return ispositive(command("AT+CREC=1," + str(id) + ",0")[-1])
else:
return ispositive(command("AT+CREC=1," + str(id) + ",0," + str(int(length/1000)))[-1])
# Stop recording audio
def stoprecording():
return ispositive(command("AT+CREC=2")[-1])
# Delete recording
def deleterecording(id=1):
return ispositive(command("AT+CREC=3," + str(id))[-1])
# Play recording
def startplayback(id=1, channel=0, level=100, repeat=False):
return ispositive(command("AT+CREC=4," + str(id) + "," + str(channel) + "," + str(level) + "," + str(int(repeat)))[-1])
# Stop playback
def stopplayback():
return ispositive(command("AT+CREC=5")[-1])
# List recordings (returns a list of ids and size)
def listrecordings():
response = command("AT+CREC=7")
responselist = extractvals("+CREC:", response)
result = []
for entry in responselist:
splitentry = entry.split(",")
result.append([splitentry[1], splitentry[2]])
return result
# Is the battery charging (0=no, 1=yes, 2=full)
def batterycharging():
response = command("AT+CBC")
vals = extractval("+CBC:", response, "0,0,0").split(",")
return int(vals[0])
# How full is the battery (1-100)
def batterycharge():
response = command("AT+CBC")
vals = extractval("+CBC:", response, "0,0,0").split(",")
return int(vals[1])
# List the available operator (returns list of [0=?,1=available,2=current,3=forbidden], 0=long name, 1=short name, 2=GSMLAI )
def listoperators(available_only=True):
delim = "||||"
response = command("AT+COPS=?", 45000)
responsedata = extractval("+COPS:", response, "").split(",,")[0]
responselist = responsedata.replace("),(",delim)[1:-1].split(delim)
results = []
for entry in responselist:
subresults = []
for subentry in entry.split(","):
subresults.append(subentry.strip("\""))
if (not available_only) or (subresults[0]=="1") or (subresults[0]=="2"):
results.append(subresults)
return results
# Get the current operator (format 0=long name, 1=short name, 2=GSMLAI)
def currentoperator(format=0):
command("AT+COPS=3," + str(format))
response = command("AT+COPS?")
responsedata = extractval("+COPS:", response, "").split(",")
if (len(responsedata)>=3):
return responsedata[2].strip("\"")
else:
return ""
# Set the operator selection ([0=automatic,1=Manual,2=deregister,4=try manual then automatic])
def setoperator(mode, format=None, operator=None):
params = ""
if format is not None:
params += "," + str(format)
if operator is not None:
params += ",\"" + str(operator) + "\""
command("AT+COPS=" + str(mode) + params, 120000)
# Get the activity status (returns 0=ready, 2=unknown, 3=ringing, 4=call in progress)
def getstatus():
response = command("AT+CPAS")
return int(extractval("+CPAS:", response, "2"))
# Get the firmware revision
def getfirmwarever():
response = command("AT+CGMR")
if (len(response)>=3):
return response[-2]
else:
return ""
# Request Unstructured Supplementary Service Data from network
def ussd(ussdstring, timeout=8000):
response = command("AT+CUSD=1,\"" + ussdstring + "\"", timeout, "+CUSD:")
return extractval("+CUSD:", response, "")
# Get my number (only works on some networks)
def getmynumber():
responsedata = ussd("*#100#", 8000).split(",")
if (len(responsedata)>=2):
num = responsedata[1].strip().strip("\"")
return num
else:
return ""
# Turn on or off Bluetooth
def btpower(onoroff=True):
command("AT+BTPOWER=" + str(int(onoroff)), 8000)
# Turn on Bluetooth
def btpoweron():
btpower(True);
# Turn off Bluetooth
def btpoweroff():
btpower(False);
# Get the current status of Bluetooth (0=off,5=idel, other values docuemtned for "AT+BTSTATUS")
def btstatus():
response = command("AT+BTSTATUS?")
return int(extractval("+BTSTATUS:", response, "0"))
# Is Bluetooth on?
def btison():
return btstatus()>=5
# Get/Set the Bluetooth host device name
def btname(name=None):
if name is not None:
response = command("AT+BTHOST=" + str(name))
# Retrieve the current name
response = command("AT+BTHOST?")
responsedata = extractval("+BTHOST:", response, "").split(",")
return responsedata[0]
# Get the Bluetooth address
def btaddress():
response = command("AT+BTHOST?")
responsedata = extractval("+BTHOST:", response, "").split(",")
if (len(responsedata)>=2):
return responsedata[-1]
else:
return ""
# Get/Set Bluetooth visibility (True for on, False for off)
def btvisible(visible=None):
# Power on if we want to be visible
if visible:
btpoweron()
# Set the new leve if we have one to set
if visible is not None:
command("AT+BTVIS=" + str(int(visible)))
# Retieve the set gain to report back
response = command("AT+BTVIS?")
return int(extractval("+BTVIS:", response, 0))
# Get the Bluetooth address (timeout in ms from 10000 to 60000, returnd device ID, name, address, rssi)
def btscan(timeout=30000):
btpoweron()
result = []
response = command("AT+BTSCAN=1," + str(int(timeout/1000)), timeout+8000, "+BTSCAN: 1")
for entry in extractvals("+BTSCAN: 0,", response):
splitentry = entry.split(",")
result.append([int(splitentry[0]), splitentry[1].strip("\""), splitentry[2], int(splitentry[3])])
return result
# Get the requesting paring device name
def btparingname():
if ison():
processbuffer()
return btpairing.split(",")[0].strip("\"")
# Get the requesting paring passcode
def btparingpasscode():
if ison():
processbuffer()
splitdata = btpairing.split(",")
if (len(splitdata)>=3):
return splitdata[2]
else:
return ""
# Pair a Bluetooth device
def btpair(device):
global btpairing
btpairing = ''
response = command("AT+BTPAIR=0," + str(device), 8000, "+BTPAIRING:")
return extractval("+BTPAIRING:", response, "").split(",")
# Confirm the pairing of a Bluetooth device
def btpairconfirm(passkey=None):
global btpairing
btpairing = ''
if passkey is None:
return command("AT+BTPAIR=1,1", 8000)
else:
return command("AT+BTPAIR=2," + str(passkey), 8000)
# Cancel/reject the pairing of a Bluetooth device
def btpairreject():
global btpairing
btpairing = ''
return command("AT+BTPAIR=1,0", 8000)
# Unpair a Bluetooth device (unpair everything when device is 0)
def btunpair(device=0):
return command("AT+BTUNPAIR=" + str(device), 8000)
# List the paired Bluetooth devices
def btpaired():
result = []
response = command("AT+BTSTATUS?")
for entry in extractvals("P:", response):
splitentry = entry.split(",")
result = [int(splitentry[0]), splitentry[1].strip("\""), splitentry[2]]
return result
# List profiles supported by a paired device
def btgetprofiles(device):
response = command("AT+BTGETPROF=" + str(device), 8000)
responselist = extractvals("+BTGETPROF:", response)
results = []
for entry in responselist:
splitentry = entry.split(",")
subresults = [int(splitentry[0]), splitentry[1].strip("\"")]
results.append(subresults)
return results
# Is a paticula profile supported (usename=False for id, True for name)
def btprofilesupported(device, usename):
profiles = btgetprofiles(device)
for entry in profiles:
if (type(usename)==int) and (entry[0]==usename):
return True
if (type(usename)==str) and (entry[1]==usename):
return True
return False
# Connect a Bluetooth device
def btconnect(device, profile):
response = command("AT+BTCONNECT=" + str(device) + "," + str(profile), 8000, "+BTCONNECT:")
return extractvals("+BTCONNECT:", response)
# Disconnect a Bluetooth device
def btdisconnect(device):
response = command("AT+BTDISCONN=" + str(device), 8000, "+BTDISCONN:")
return extractvals("+BTDISCONN:", response)
# List the Bluetooth connections
def btconnected():
result = []
response = command("AT+BTSTATUS?")
for entry in extractvals("C:", response):
splitentry = entry.split(",")
result = [int(splitentry[0]), splitentry[1].strip("\""), splitentry[2]]
return result
# Push an OPP object/file over Bluetooth (must be paired for OPP, monitor +BTOPPPUSH: for sucsess / fail / server issue)
def btopppush(device, filename):
response = command("AT+BTOPPPUSH=" + str(device) + "," + filename, 45000, "+BTOPPPUSH:")
responce2 = extractval("+BTOPPPUSH:", response, "")
return responce2=="1"
# Accept an OPP object/file from Bluetooth (monitor +BTOPPPUSHING: for offering, files stored in "\\User\\BtReceived")
def btoppaccept():
response = command("AT+BTOPPACPT=1", 45000, "+BTOPPPUSH:")
responce2 = extractval("+BTOPPPUSH:", response, 0)
return responce2=="1"
# Send data over a Bluetooth serial connection
def btsppwrite(connection, data):
response = command("AT+BTSPPSEND=" + str(connection) + "," + str(len(data)), 8000, None, ">")
if response[-1].startswith(">"):
return ispositive(command(data)[-1])
else:
return False
# Receive data from a Bluetooth serial connection
def btsppread(connection):
command()
global holdoffirq
# Don't let the interupt process the buffer mid command
holdoffirq = True
request = "AT+BTSPPGET=3," + str(connection) + "\n"
uart.write(request)
data = uart.read()
while True:
time.sleep(uart_timeout/1000)
if uart.any()==0:
break
data += uart.read()
holdoffirq = False
if not data.endswith("ERROR\r\n"):
return data[len(request)+2:-6]
else:
return None
# Reject an OPP object/file transfer
def btoppreject():
command("AT+BTOPPACPT=0")
# Make a voice call
def btcall(number):
btpoweron()
command("AT+BTATD" + str(number), 20000)
# Answer a voice call
def btanswer():
btpoweron()
command("AT+BTATA", 20000)
# End a voice call
def bthangup():
btpoweron()
command("AT+BTATH")
# Redial the last number
def btredial():
btpoweron()
command("AT+BTATDL")
# Play DTMF tone(s) on a Bluetooth call
def btdtmf(number):
validdigits = '1234567890#*ABCD'
for digit in str(number).upper():
if (digit in validdigits):
command("AT+BTVTS=" + digit)
elif (digit==','):
time.sleep(1)
# Get/Set Bluetooth voice gain (0-15)
def btvoicevolume(gain=None):
# Set the new leve if we have one to set
if gain is not None:
command("AT+BTVGS=" + str(gain))
# Retieve the set gain to report back
response = command("AT+BTVGS?")
return int(extractval("+BTVGS:", response, 0))
# Get/Set microphone gain volume (0-15)
def btvoicevolume(gain=None):
# Set the new leve if we have one to set
if gain is not None:
command("AT+BTVGM=" + str(gain))
# Retieve the set gain to report back
response = command("AT+BTVGM?")
return int(extractval("+BTVGM:", response, 0))
# Get the Bluetooth signal quality for a device (-127-0)
def btrssi(device):
response = command("AT+BTRSSI=" + str(device))
return int(extractval("+BTRSSI:", response, 0))
# Get available space on the flash storage
def fsfree():
response = command("AT+FSMEM")
return extractval("+FSMEM:", response, "?:0bytes").split(",")[0].split(":")[1][:-5]
# List the entries in directory on flash storage (returned directories end with "\\")
def fsls(directory=""):
if not directory.endswith("\\"):
directory += "\\"
return command("AT+FSLS=" + str(directory))[1:-1]
# Get the size of a file on the flash storage
def fssize(filename):
response = command("AT+FSFLSIZE=" + str(filename))
return int(extractval("+FSFLSIZE:", response, "-1"))
# Create a directory on flash storage
def fsmkdir(directory):
return ispositive(command("AT+FSMKDIR=" + str(directory))[-1])
# Remove a directory on flash storage
def fsrmdir(directory):
return ispositive(command("AT+FSRMDIR=" + str(directory))[-1])
# Create a file on flash storage
def fscreate(filename):
return ispositive(command("AT+FSCREATE=" + str(filename))[-1])
# Read a chunk of data from a file on the flash storage
def fsreadpart(filename, size=256, start=0):
global holdoffirq
mode=int(start>0)
command()
# Don't let the interupt process the buffer mid command
holdoffirq = True
request = "AT+FSREAD=" + str(filename) + "," + str(mode) + "," + str(size) + "," + str(start) + "\n"
uart.write(request)
data = uart.read()
while True:
time.sleep(uart_timeout/1000)
if uart.any()==0:
break
data += uart.read()
holdoffirq = False
if not data.endswith("ERROR\r\n"):
return data[len(request)+2:-6]
else:
return None
# Read data from a file on the flash storage
def fsread(filename):
result = bytearray(0)
while True:
chunk = fsreadpart(filename, 256, len(result))
if chunk is not None:
result += chunk
else:
return result
# Append a small chunk data to a file on the flash storage, you should use sfwrite
def fswritepart(filename, data):
response = command("AT+FSWRITE=" + str(filename) + ",1," + str(len(data)) + ",8", 2000, None, ">")
if response[-1].startswith(">"):
return ispositive(command(data)[-1])
else:
return False
# Write data to a file on the flash storage
def fswrite(filename, data, truncate=False):
length = len(data)
pointer = 0
chunksize = 256
# Create a file if needed
if truncate or (fssize(filename)<0):
fscreate(filename)
# Loop through the data in small chunks
while pointer<length:
result = fswritepart(filename, data[pointer:min(pointer+chunksize,length)])
if not result:
return False
pointer += chunksize
return True
# Delete a file from flash storage
def fsrm(filename):
return ispositive(command("AT+FSDEL=" + str(filename))[-1])
# Rename a file on the flash storage
def fsmv(filenamefrom, filenameto):
return ispositive(command("AT+FSRENAME=" + str(filenamefrom) + "," + str(filenameto))[-1])
# Callback for call buton being pressed
def callbuttonpressed_internal(nullparam=None):
answer()
# Callback for end buton being pressed
def endbuttonpressed_internal(nullparam=None):
hangup()
#GPRS and TCP server functions
def setup_gprs():
command("AT+CIPSHUT", response_timeout=60000, custom_endofdata="SHUT OK")
command("AT+CGATT?", response_timeout=10000)
command("AT+CIPMUX=1", response_timeout=10000)
def connect_gprs(apn):
command("AT+CSTT=\""+apn+"\"", response_timeout=10000)
command("AT+CIICR", response_timeout=10000)
command("AT+CIFSR")
def stop_gprs():
command("AT+CIPSHUT", response_timeout=60000, custom_endofdata="SHUT OK")
def start_server(port, callback):
global server_callback
server_callback = callback
command("AT+CIPSERVER=1,"+str(port), response_timeout=10000)
def stop_server():
command("AT+CIPSERVER=0", response_timeout=10000)
# Startup...
# Start turning on the SIM800 asynchronously
onatstart = poweron(True)
# Reset SIM800 configuration if hardware is still on from before
if onatstart:
senddefaultconfig()
# Turn on the audio amp
amp_pin.on()
# Hook in the Call / End buttons
tilda.Buttons.enable_interrupt(tilda.Buttons.BTN_Call, callbuttonpressed_internal)
tilda.Buttons.enable_interrupt(tilda.Buttons.BTN_End, endbuttonpressed_internal)
# Enable the interupts on network light to poll uart
netlight_pin.irq(netlightirq_internal)

21
lib/sleep.py Normal file
View File

@ -0,0 +1,21 @@
"""Library for sleep related functions"""
___license___ = "MIT"
import time
def sleep_ms(duration):
start_time = time.ticks_ms()
end_time = start_time + duration
while time.ticks_ms() < end_time:
wfi()
def sleep(duration):
sleep_ms(duration * 1000)
def wfi():
# todo: this is fake
time.sleep_ms(1)

71
lib/speaker.py Normal file
View File

@ -0,0 +1,71 @@
"""Library for speaker related functions"""
from machine import Pin, PWM
from math import log, pow
from time import sleep_ms
_enabled = True
_amp_pin = Pin(Pin.GPIO_AMP_SHUTDOWN)
_speaker_pwm = None
def enabled(value=None):
"""Sets the internal amplifier. This is entirely independent from the PWM"""
global _enabled
if value == None:
return _enabled
else:
_enabled = value
_set_amp()
def _set_amp():
if enabled():
_amp_pin.on()
else:
_amp_pin.off()
def speaker_pwm():
global _speaker_pwm
if not _speaker_pwm:
_speaker_pwm = PWM(PWM.PWM_SPEAKER)
return _speaker_pwm
_frequency = None
def frequency(value = None):
global _frequency
if value == None:
return _frequency
_frequency = value
_set_amp()
speaker_pwm().init(duty=1, freq=_frequency)
def stop():
global _frequency
# todo: maybe we should deinit the PWM?
_frequency = None
_set_amp()
speaker_pwm().duty(0)
# Music
NOTES = {"C":0, "C#":1, "D":2, "D#":3, "E":4, "F":5, "F#":6, "G":7, "G#":8, "A":9, "A#":10, "B":11}
def note_to_frequency(note):
if (len(note) == 2) and note[1].isdigit():
octave = int(note[1])
note = note[0].upper()
elif len(note) == 3:
octave = int(note[2])
note = note[0:2].upper()
else:
octave = 4
note = note.upper()
if note not in NOTES:
raise Exception("%s it not a valid note" % note)
halftones = NOTES[note] + 12 * (octave - 4)
freq = 440 * pow(1.059, halftones)
return round(freq)
def note(note):
frequency(note_to_frequency(note))

63
lib/stack_nav.py Normal file
View File

@ -0,0 +1,63 @@
"""A stack-based naviation system.
Maintains a stack of states through which a user can navigate.
Think Android's back button model.
Every screen is modeled as a function that takes one argument "state".
It can modify state and should either return
* None (which means go back one level)
* A new function (which adds to the stack)
* A keyword like NEXT_EXIT (exit nav) or NEXT_TOP (go to top of stack)
Children get a a shallow clone of the current state, going back will
lead to a screen with the original state.
"""
___dependencies___ = ["dialogs", "database"]
___license___ = "MIT"
import dialogs, database
NEXT_EXIT = "exit" # Leave navigation
NEXT_TOP = "init" # Go to top of stack
def nav(init_fn, init_state={}):
stack = [(init_fn, init_state)]
while stack:
(fn, state) = stack[-1] # peek
next_state = state.copy()
result = fn(next_state)
if callable(result):
stack.append((result, next_state))
elif result == NEXT_TOP:
stack = [(init_fn, init_state)]
elif result == NEXT_EXIT:
break
else:
stack.pop()
# A simple menu. Format of items is [{"foo":fn}, ...]
def selection(items, title="Settings", none_text="Back"):
items = [{"title": t, "function": fn} for (t, fn) in items.items()]
selection = dialogs.prompt_option(items, none_text=none_text, title=title)
if selection:
return selection["function"]
else:
return None
# A wrapper for a simple string menu
def change_string(title, getter_setter_function):
def inner(state):
value = getter_setter_function()
value = dialogs.prompt_text(title, init_text=value, true_text="Change", false_text="Back")
if value:
getter_setter_function(value)
return inner
# A wrapper for a database key
def change_database_string(title, key, default=""):
def inner(state):
value = database.get(key, default)
value = dialogs.prompt_text(title, init_text=value, true_text="Change", false_text="Back")
if value:
database.set(key, value)
return inner

View File

@ -3,34 +3,59 @@
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "badge_store", "shared/test/file.txt"]
import unittest
import unittest, os
from lib.badge_store import *
from ospath import *
class TestBadgeStore(unittest.TestCase):
def setUpClass(self):
self.store = BadgeStore(url="http://badge.marekventur.com", repo="emfcamp/Mk4-Apps", ref="ee144e8")
self.store = BadgeStore(repo="emfcamp/Mk4-Apps", ref="ee144e8")
self.download_file = "shared/test/download.txt"
def test_apps(self):
response = self.store.get_apps()
def tearDownClass(self):
self._remove_download_file()
def test_get_all_apps(self):
response = self.store.get_all_apps()
self.assertEqual(response["System"], ['badge_store', 'launcher', 'settings'])
def test_categories(self):
categories = self.store.get_categories()
self.assertEqual(set(categories), set(["System", "homescreen"]))
def test_app(self):
def test_get_apps(self):
apps = self.store.get_apps("System")
self.assertEqual(set(apps), set(['badge_store', 'settings', 'launcher']))
def test_get_app(self):
response = self.store.get_app("launcher")
self.assertEqual(response["description"], "Launcher for apps currently installed")
def test_is_file_up_to_date(self):
self.assertFalse(self.store._is_file_up_to_date("shared/test/file.txt", "1234567890"))
self.assertFalse(self.store._is_file_up_to_date("does/not/exist.txt", "1234567890"))
self.assertTrue(self.store._is_file_up_to_date("shared/test/file.txt", "182d04f8ee"))
def test_get_hash(self):
self.assertEqual(get_hash("shared/test/file.txt"), "182d04f8ee")
self.assertEqual(get_hash("does/not/exist.txt"), None)
def test_install(self):
installers = self.store.install(["launcher", "badge_store"])
def test_install_integration(self):
self._remove_download_file()
store = BadgeStore(url="http://badge.marekventur.com", repo="emfcamp/Mk4-Apps", ref="dont-delete-test-download-branch")
for installer in store.install(["launcher"]):
if installer.path == "shared/test/download.txt":
installer.download()
with open(self.download_file, "rt") as response:
self.assertIn("I'm a download test", response.read())
def test_bootstrap_integration(self):
self._remove_download_file()
store = BadgeStore(url="http://badge.marekventur.com")
installers = store.bootstrap()
self.assertTrue(len(installers) > 0)
def _remove_download_file(self):
if isdir(self.download_file) or isfile(self.download_file):
os.remove(self.download_file)
if __name__ == '__main__':
unittest.main()

20
lib/test_buttons.py Normal file
View File

@ -0,0 +1,20 @@
"""Tests for icons lib"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "buttons"]
import unittest, ugfx, time
from buttons import *
class TestButtons(unittest.TestCase):
def test_is_pressed(self):
self.assertFalse(is_pressed(Buttons.BTN_9))
def test_is_triggered(self):
self.assertFalse(is_triggered(Buttons.BTN_9))
if __name__ == '__main__':
unittest.main()

38
lib/test_dialogs.py Normal file
View File

@ -0,0 +1,38 @@
"""Tests for app lib
Very limited at the moment since we can't test the main input dialogs"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "dialogs", "sleep", "ugfx_helper"]
import unittest, ugfx, ugfx_helper
from machine import Pin
from dialogs import *
from sleep import *
class TestDialogs(unittest.TestCase):
def setUpClass(self):
ugfx_helper.init()
def tearDownClass(self):
ugfx_helper.deinit()
def test_waiting(self):
count_max = 3
with WaitingMessage("Testing...", "Foo") as c:
for i in range(1, count_max):
c.text = "%d/%d" % (i, count_max)
print("done")
def test_text(self):
prompt_text("description")
def test_option(self):
print(prompt_option(["foo", "bar", "baz"]))
if __name__ == '__main__':
unittest.main()

16
lib/test_hall_effect.py Normal file
View File

@ -0,0 +1,16 @@
"""Tests for hall effect sensor"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "hall_effect"]
import unittest, hall_effect, speaker
class TestHallEffect(unittest.TestCase):
def test_hall(self):
flux = hall_effect.get_flux()
self.assertTrue(flux > 0)
self.assertTrue(flux < 4000)
if __name__ == '__main__':
unittest.main()

27
lib/test_homescreen.py Normal file
View File

@ -0,0 +1,27 @@
"""Tests for hall effect sensor"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "homescreen", "database"]
import unittest, homescreen, database
class TestHomescreen(unittest.TestCase):
def test_name(self):
o = database.get("homescreen.name")
database.delete("homescreen.name")
self.assertEqual(homescreen.name("default"), "default")
database.set("homescreen.name", "foo")
self.assertEqual(homescreen.name("default"), "foo")
database.set("homescreen.name", o)
def test_time(self):
self.assertIn(len(homescreen.time_as_string()), [4, 5])
self.assertIn(len(homescreen.time_as_string(True)), [7, 8])
def test_wifi_strength(self):
# test that it doesn't throw an exception
homescreen.wifi_strength()
if __name__ == '__main__':
unittest.main()

View File

@ -13,9 +13,9 @@ class TestHttp(unittest.TestCase):
wifi.connect()
def test_get_with_https(self):
with self.assertRaises(OSError) as context:
get("https://httpbin.org/get")
self.assertIn("HTTPS is currently not supported", str(context.exception))
with get("https://httpbin.org/get") as response:
self.assertEqual(response.status, 200)
print(response.text)
def test_get(self):
with get("http://httpbin.org/get", params={"foo": "bar"}, headers={"accept": "application/json"}) as response:

View File

@ -9,29 +9,29 @@ from icons import *
class TestIcons(unittest.TestCase):
# incomplete!
# todo: fix me
def setUp(self):
ugfx.init()
ugfx.orientation(90)
ugfx.clear()
def test_icon(self):
icon = Icon(44, 40, "Badge Store with", "badge_store/icon.gif")
icon.show()
for s in [True, False, True]:
icon.selected = s
time.sleep(0.1)
icon.__del__()
def test_icon_grid(self):
items = []
for i in range(50):
items.append({
"title": "App %s" % i
})
icon_grid = IconGrid(5, 5, items, None)
# def test_icon(self):
# icon = Icon(44, 40, "Badge Store with", "badge_store/icon.gif")
# icon.show()
#
# for s in [True, False, True]:
# icon.selected = s
# time.sleep(0.1)
#
# icon.__del__()
#
# def test_icon_grid(self):
# items = []
# for i in range(50):
# items.append({
# "title": "App %s" % i
# })
# icon_grid = IconGrid(5, 5, items, None)

29
lib/test_ntp.py Normal file
View File

@ -0,0 +1,29 @@
"""Tests for ntp"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "ntp", "wifi", "ugfx_helper"]
import unittest, wifi, ntp, machine, ugfx_helper
class TestWifi(unittest.TestCase):
def setUpClass(self):
ugfx_helper.init()
wifi.connect()
def tearDownClass(self):
ugfx_helper.deinit()
def test_get_time(self):
t = ntp.get_NTP_time()
self.assertTrue(t > 588699276)
self.assertTrue(t < 1851003302) # 27 August 2028
def test_set_time(self):
ntp.set_NTP_time()
rtc = machine.RTC()
self.assertTrue(rtc.now()[0] >= 2018)
self.assertTrue(rtc.now()[0] <= 2028)
if __name__ == '__main__':
unittest.main()

39
lib/test_random.py Normal file
View File

@ -0,0 +1,39 @@
"""Tests for random lib"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest"]
import unittest
from random import *
class TestRandom(unittest.TestCase):
def test_random(self):
for i in range(1, 100):
r = random()
self.assertTrue(r>=0)
self.assertTrue(r<=1)
def test_randint(self):
for i in range(1, 100):
r = randint(500, 1000)
self.assertTrue(r>=500)
self.assertTrue(r<=1000)
def test_randrange(self):
for i in range(1, 100):
r = randrange(10)
self.assertTrue(r>=0)
self.assertTrue(r<10)
def test_shuffle(self):
for i in range(1, 100):
r = list(range(1, 10))
shuffle(r)
self.assertEqual(sum(r), 45)
self.assertEqual(set(r), set(range(1, 10)))
self.assertNotEqual(r, list(range(1, 10)))
if __name__ == '__main__':
unittest.main()

26
lib/test_sleep.py Normal file
View File

@ -0,0 +1,26 @@
"""Tests for http"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "sleep"]
import unittest, sleep, time
class TestSleep(unittest.TestCase):
def test_sleep(self):
sleep_secs = 5
time_before = time.ticks_ms()
time_after = time_before + 1000 * sleep_secs
sleep.sleep(sleep_secs)
self.assertTrue(time.ticks_ms() >= time_after)
def test_sleep_ms(self):
sleep_ms = 3000
time_before = time.ticks_ms()
time_after = time_before + sleep_ms
sleep.sleep_ms(sleep_ms)
self.assertTrue(time.ticks_ms() >= time_after)
if __name__ == '__main__':
unittest.main()

34
lib/test_speaker.py Normal file
View File

@ -0,0 +1,34 @@
"""Tests for http"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "speaker", "sleep"]
import unittest, speaker, ugfx_helper
from sleep import *
class TestSpeaker(unittest.TestCase):
def tearDown(self):
speaker.stop()
def test_enabled(self):
self.assertEqual(speaker.enabled(), True)
speaker.enabled(False)
self.assertEqual(speaker.enabled(), False)
speaker.enabled(True)
self.assertEqual(speaker.enabled(), True)
def test_beep_and_stop(self):
for f in range(50, 1000):
speaker.frequency(f)
self.assertEqual(speaker.frequency(), f)
sleep_ms(1)
speaker.stop()
def test_note(self):
for n in ["c", "d", "e", "f", "g", "a", "b", "c5"]:
speaker.note(n)
sleep_ms(100)
if __name__ == '__main__':
unittest.main()

23
lib/test_websockets.py Normal file
View File

@ -0,0 +1,23 @@
"""Tests for http"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "websockets", "wifi"]
import unittest
from websockets import connect
import wifi
class TestWebsockets(unittest.TestCase):
def setUpClass(self):
wifi.connect()
def test_echo_service(self):
ws = connect("ws://echo.websocket.org")
self.assertTrue(ws.open)
ws.send("Hello from TiLDA")
response = ws.recv()
self.assertEqual(response, "Hello from TiLDA")
if __name__ == '__main__':
unittest.main()

22
lib/test_wifi.py Normal file
View File

@ -0,0 +1,22 @@
"""Tests for wifi"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "wifi", "ugfx_helper"]
import unittest, wifi, ugfx_helper
from machine import Pin
class TestWifi(unittest.TestCase):
def setUpClass(self):
ugfx_helper.init()
def tearDownClass(self):
ugfx_helper.deinit()
def test_connect(self):
wifi.connect(show_wait_message=True)
self.assertTrue(wifi.is_connected())
if __name__ == '__main__':
unittest.main()

13
lib/ugfx_helper.py Normal file
View File

@ -0,0 +1,13 @@
"""Helper library for ugfx stuff"""
___license___ = "MIT"
import ugfx
from machine import Pin
def init():
ugfx.init()
Pin(Pin.PWM_LCD_BLIGHT).on()
def deinit():
Pin(Pin.PWM_LCD_BLIGHT).off()

324
lib/websockets.py Normal file
View File

@ -0,0 +1,324 @@
"""
Websockets client for micropython
Based off https://github.com/danni/uwebsockets
"""
import ure as re
import ubinascii as binascii
import ustruct as struct
import urandom as random
import usocket as socket
from ucollections import namedtuple
# Opcodes
OP_CONT = const(0x0)
OP_TEXT = const(0x1)
OP_BYTES = const(0x2)
OP_CLOSE = const(0x8)
OP_PING = const(0x9)
OP_PONG = const(0xa)
# Close codes
CLOSE_OK = const(1000)
CLOSE_GOING_AWAY = const(1001)
CLOSE_PROTOCOL_ERROR = const(1002)
CLOSE_DATA_NOT_SUPPORTED = const(1003)
CLOSE_BAD_DATA = const(1007)
CLOSE_POLICY_VIOLATION = const(1008)
CLOSE_TOO_BIG = const(1009)
CLOSE_MISSING_EXTN = const(1010)
CLOSE_BAD_CONDITION = const(1011)
URL_RE = re.compile(r'ws://([A-Za-z0-9\-\.]+)(?:\:([0-9]+))?(/.+)?')
URI = namedtuple('URI', ('hostname', 'port', 'path'))
DEBUG = False
# Call this to spew the headers etc
def enable_debug():
global DEBUG
DEBUG = True
def debug_log(*args, **kwargs):
if DEBUG:
print(args)
def urlparse(uri):
"""Parse ws:// URLs"""
match = URL_RE.match(uri)
if match:
hostname = match.group(1)
port = int(match.group(2)) if match.group(2) else 80
path = match.group(3)
return URI(hostname, port, path)
class Websocket:
"""
Basis of the Websocket protocol.
This can probably be replaced with the C-based websocket module, but
this one currently supports more options.
"""
is_client = False
def __init__(self, sock):
self._sock = sock
self.open = True
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
debug_log("exited context manager")
self.close()
def settimeout(self, timeout):
self._sock.settimeout(timeout)
def read_frame(self, max_size=None):
"""
Read a frame from the socket.
See https://tools.ietf.org/html/rfc6455#section-5.2 for the details.
"""
# Frame header
byte1, byte2 = struct.unpack('!BB', self._sock.recv(2))
debug_log("read a frame header: {} {}".format(byte1, byte2))
# Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4)
fin = bool(byte1 & 0x80)
opcode = byte1 & 0x0f
# Byte 2: MASK(1) LENGTH(7)
mask = bool(byte2 & (1 << 7))
length = byte2 & 0x7f
if length == 126: # Magic number, length header is 2 bytes
debug_log("length is 126")
length, = struct.unpack('!H', self._sock.recv(2))
elif length == 127: # Magic number, length header is 8 bytes
debug_log("length is 127")
length, = struct.unpack('!Q', self._sock.recv(8))
else:
debug_log("length is something else: {}".format(length))
if mask: # Mask is 4 bytes
mask_bits = self._sock.recv(4)
if length:
debug_log("Trying to receive {} bytes".format(length))
try:
data = self._sock.recv(length)
except MemoryError:
# We can't receive this many bytes, close the socket
debug_log("Frame of length %s too big. Closing",
length)
self.close(code=CLOSE_TOO_BIG)
return True, OP_CLOSE, None
else:
data = b''
if mask:
data = bytes(b ^ mask_bits[i % 4]
for i, b in enumerate(data))
return fin, opcode, data
def write_frame(self, opcode, data=b''):
"""
Write a frame to the socket.
See https://tools.ietf.org/html/rfc6455#section-5.2 for the details.
"""
fin = True
mask = self.is_client # messages sent by client are masked
length = len(data)
# Frame header
# Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4)
byte1 = 0x80 if fin else 0
byte1 |= opcode
# Byte 2: MASK(1) LENGTH(7)
byte2 = 0x80 if mask else 0
if length < 126: # 126 is magic value to use 2-byte length header
byte2 |= length
self._sock.send(struct.pack('!BB', byte1, byte2))
elif length < (1 << 16): # Length fits in 2-bytes
byte2 |= 126 # Magic code
self._sock.send(struct.pack('!BBH', byte1, byte2, length))
elif length < (1 << 64):
byte2 |= 127 # Magic code
self._sock.send(struct.pack('!BBQ', byte1, byte2, length))
else:
raise ValueError()
if mask: # Mask is 4 bytes
mask_bits = struct.pack('!I', random.getrandbits(32))
self._sock.send(mask_bits)
data = bytes(b ^ mask_bits[i % 4]
for i, b in enumerate(data))
self._sock.send(data)
def recv(self):
"""
Receive data from the websocket.
This is slightly different from 'websockets' in that it doesn't
fire off a routine to process frames and put the data in a queue.
If you don't call recv() sufficiently often you won't process control
frames.
"""
assert self.open
while self.open:
try:
fin, opcode, data = self.read_frame()
except ValueError:
debug_log("Failed to read frame. Socket dead.")
self._close()
return
debug_log("Got a frame with opcode {}".format(opcode))
if not fin:
raise NotImplementedError()
if opcode == OP_TEXT:
return data.decode('utf-8')
elif opcode == OP_BYTES:
return data
elif opcode == OP_CLOSE:
debug_log("got a close frame")
self._close()
return
elif opcode == OP_PONG:
# Ignore this frame, keep waiting for a data frame
continue
elif opcode == OP_PING:
# We need to send a pong frame
debug_log("Sending PONG")
self.write_frame(OP_PONG, data)
# And then wait to receive
continue
elif opcode == OP_CONT:
# This is a continuation of a previous frame
raise NotImplementedError(opcode)
else:
raise ValueError(opcode)
def send(self, buf):
"""Send data to the websocket."""
assert self.open
if isinstance(buf, str):
opcode = OP_TEXT
buf = buf.encode('utf-8')
elif isinstance(buf, bytes):
opcode = OP_BYTES
else:
raise TypeError()
self.write_frame(opcode, buf)
def close(self, code=CLOSE_OK, reason=''):
"""Close the websocket."""
if not self.open:
return
buf = struct.pack('!H', code) + reason.encode('utf-8')
self.write_frame(OP_CLOSE, buf)
self._close()
def _close(self):
debug_log("Connection closed")
self.open = False
self._sock.close()
def readline(sock):
# Micropython on the Mk3 doesn't have socket.readline
try:
line = sock.readline()[:-2]
debug_log("readline:")
debug_log(line)
return line
except AttributeError:
line = b''
while True:
chunk = sock.recv(2)
if chunk == b'\r\n':
debug_log("Read line:")
debug_log(line)
return line
else:
line += chunk
class WebsocketClient(Websocket):
is_client = True
def connect(uri):
"""
Connect a websocket.
"""
uri = urlparse(uri)
assert uri
debug_log("open connection %s:%s",
uri.hostname, uri.port)
sock = socket.socket()
debug_log("got a socket")
try:
addr = socket.getaddrinfo(uri.hostname, uri.port)
debug_log("getaddrinfo result")
debug_log(addr)
sock.connect(addr[0][4])
debug_log("connected using getaddrinfo method")
except OSError:
debug_log("getaddrinfo method failed")
sock.connect((uri.hostname, uri.port))
debug_log("connected using direct method")
def send_header(header, *args):
debug_log(str(header), *args)
sock.send(header % args + '\r\n')
# Sec-WebSocket-Key is 16 bytes of random base64 encoded
key = binascii.b2a_base64(bytes(random.getrandbits(8)
for _ in range(16)))[:-1]
debug_log("set key to:")
debug_log(key)
send_header(b'GET %s HTTP/1.1', uri.path or '/')
send_header(b'Host: %s:%s', uri.hostname, uri.port)
send_header(b'Connection: Upgrade')
send_header(b'Upgrade: websocket')
send_header(b'Sec-WebSocket-Key: %s', key)
send_header(b'Sec-WebSocket-Version: 13')
send_header(b'Origin: http://localhost')
send_header(b'')
header = readline(sock)
assert header[:13] == b'HTTP/1.1 101 ', header
# We don't (currently) need these headers
# FIXME: should we check the return key?
while header:
debug_log(str(header))
header = readline(sock)
return WebsocketClient(sock)

View File

@ -1,20 +1,17 @@
"""Handles connecting to a wifi access point based on a valid wifi.json file"""
___license___ = "MIT"
___dependencies___ = ["dialogs"]
___dependencies___ = ["dialogs", "sleep"]
import network
import os
import json
import pyb
import dialogs
import network, os, json, dialogs, sleep, time
_nic = None
def nic():
global _nic
if not _nic:
_nic = network.CC3100()
_nic = network.WLAN()
_nic.active(True)
return _nic
def connection_details():
@ -34,10 +31,8 @@ def ssid():
return connection_details()["ssid"]
def connect(wait=True, timeout=10, show_wait_message=False, prompt_on_fail=True, dialog_title='TiLDA'):
retry_connect = True
while retry_connect:
if nic().is_connected():
while True:
if nic().isconnected():
return
details = connection_details()
@ -63,62 +58,78 @@ def connect(wait=True, timeout=10, show_wait_message=False, prompt_on_fail=True,
text="Failed to connect to '%s'" % details['ssid'],
title=dialog_title,
true_text="Try again",
false_text="Forget it",
false_text="Change it",
)
if not retry_connect:
os.remove('wifi.json')
os.sync()
# We would rather let you choose a new network here, but
# scanning doesn't work after a connect at the moment
pyb.hard_reset()
else:
raise
def connect_wifi(details, timeout, wait=False):
if 'pw' in details:
nic().connect(details['ssid'], details['pw'], timeout=timeout)
if 'user' in details:
nic().connect(details['ssid'], details['pw'], enterprise=True, entuser=details['user'], entmethod=nic().EAP_METHOD_PEAP0_MSCHAPv2, entserverauth=False)
elif 'pw' in details:
nic().connect(details['ssid'], details['pw'])
else:
nic().connect(details['ssid'], timeout=timeout)
nic().connect(details['ssid'])
if wait:
while not nic().is_connected():
nic().update()
pyb.delay(100)
wait_until = time.ticks_ms() + timeout * 1000
while not nic().isconnected():
#nic().update() # todo: do we need this?
if (time.ticks_ms() > wait_until):
raise OSError("Timeout while trying to connect to wifi")
sleep.sleep_ms(100)
def is_connected():
return nic().is_connected()
return nic().isconnected()
# returns wifi strength in %, None if unavailable
def get_strength():
n = nic()
if n.isconnected():
v = n.status("rssi");
if v:
# linear range: -60 =100%; -100= 20%
# todo: it's probably not linear, improve me.
return v * 2 + 220
return None
def get_security_level(ap):
#todo: fix this
n = nic()
levels = {}
try:
levels = {
n.SCAN_SEC_OPEN: 0, # I am awful
n.SCAN_SEC_WEP: 'WEP',
n.SCAN_SEC_WPA: 'WPA',
n.SCAN_SEC_WPA2: 'WPA2',
}
except AttributeError:
print("Firmware too old to query wifi security level, please upgrade.")
return None
levels = {
n.SCAN_SEC_OPEN: 0, # I am awful
n.SCAN_SEC_WEP: 'WEP',
n.SCAN_SEC_WPA: 'WPA',
n.SCAN_SEC_WPA2: 'WPA2',
}
return levels.get(ap.get('security', None), None)
def choose_wifi(dialog_title='TiLDA'):
filtered_aps = []
with dialogs.WaitingMessage(text='Scanning for networks...', title=dialog_title):
visible_aps = nic().list_aps()
visible_aps.sort(key=lambda x:x['rssi'], reverse=True)
visible_aps = None
while not visible_aps:
visible_aps = nic().scan()
print(visible_aps)
sleep.sleep_ms(300)
#todo: timeout
visible_aps.sort(key=lambda x:x[3], reverse=True)
# We'll get one result for each AP, so filter dupes
for ap in visible_aps:
title = ap['ssid']
security = get_security_level(ap)
title = ap[0]
security = "?" # todo: re-add get_security_level(ap)
if security:
title = title + ' (%s)' % security
ap = {
'title': title,
'ssid': ap['ssid'],
'security': security,
'ssid': ap[0],
'security': ap[4],
}
if ap['ssid'] not in [ a['ssid'] for a in filtered_aps ]:
filtered_aps.append(ap)
@ -136,11 +147,7 @@ def choose_wifi(dialog_title='TiLDA'):
if ap['security'] == None:
ap['security'] = 'wifi'
key = dialogs.prompt_text(
"Enter %s key" % ap['security'],
width = 310,
height = 220
)
key = dialogs.prompt_text("Enter %s key" % ap['security'])
with open("wifi.json", "wt") as file:
if key:
conn_details = {"ssid": ap['ssid'], "pw": key}
@ -149,5 +156,4 @@ def choose_wifi(dialog_title='TiLDA'):
file.write(json.dumps(conn_details))
os.sync()
# We can't connect after scanning for some bizarre reason, so we reset instead
pyb.hard_reset()
# todo: last time we had to hard reset here, is that still the case?

BIN
lobstervision/.main.py.swp Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More