1
0
mirror of https://github.com/javierpena/eink-calendar.git synced 2025-08-10 21:52:01 +02:00

First upload

This commit is contained in:
Javier Peña
2021-01-16 15:29:10 +01:00
parent c14a21137c
commit 4f82ac0cbd
23 changed files with 1373 additions and 15 deletions

140
.gitignore vendored Normal file
View File

@@ -0,0 +1,140 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/

27
LICENSE
View File

@@ -1,25 +1,26 @@
BSD 2-Clause License Copyright (c) <year> <owner>. All rights reserved.
Copyright (c) 2021, Javier Peña Redistribution and use in source and binary forms, with or without modification,
All rights reserved. are permitted provided that the following conditions are met:
Redistribution and use in source and binary forms, with or without 1. Redistributions of source code must retain the above copyright notice,
modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, 2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

111
README.md Normal file
View File

@@ -0,0 +1,111 @@
# Eink family calendar
## What is it?
I needed to have an updatable calendar to keep track of my children's school agenda,
as well as some post-school activities. So instead of using a whiteboard, I thought
it would be a nice holiday project to do it with a Raspberry Pi 2 and an e-ink
screen.
## What do you need
- A Raspberry Pi (any model would do, I did it with a Pi 2), with the Raspberry Pi OS.
- Eink display: [640x384, 7.5inch E-Ink display HAT for Raspberry Pi, yellow/black/white three-color](https://www.waveshare.com/product/displays/e-paper/epaper-1/7.5inch-e-paper-hat-c.htm).
- A [1x4 matrix keypad](https://www.adafruit.com/product/1332) to allow you to switch
between the different calendars. There are multiple clones in different shops.
- A photo frame to house the setup. I bought one at a local shop, just make sure it has
enough depth to host all the hardware.
- One or more Caldav calendars to display.
## Configuring
Once the basic setup and connections are done (TODO: add diagram for GPIO connections),
you will need to set up the `config.ini` file. This is the syntax:
```ini
[DEFAULT]
video = pygame
keyboard = pygame
[weather]
owm_api_key = <insert API key here>
owm_location = <insert location id here>
[calendar]
urls = http://example.com/caldavcal1, http://example.com/caldavcal2, http://example.com/caldavcal3
names = user1, user2, user3
username = user
password = password
```
### Driver configuration
We have two different drivers to be used for both video output and keyboard input.
This allows us to hack and tests locally without using the e-ink screen all the time.
- For video, we can use `pygame` for the [Pygame](https://github.com/pygame/pygame)
driver, or `eink` to use the e-ink screen.
- For keyboard input, we can use `pygame` to get input from your computer's keyboard,
or `gpio` to use the 1x4 keypad connected to the GPIO pins 29 ,31, 33, 35 and 37.
### Open Weather Map API configuration
You need to subscribe to the "Current Weather Data" API [link](https://openweathermap.org/api).
Note that you need to sign in as a user (it's free). Once you get the API key and the
location id for your town, add them to the `owm_api_key` and `owm_location` keys in
the configuration file.
### Calendar configuration
You can use any CalDav link; in my case, I set up 3 calendars in a [Synology](https://www.synology.com)
DiskStation. The code should adapt to any number of calendars, but keep in mind the
resolution and font size ;).
The `urls` parameter is a comma-separated list of CalDav urls. Since each of those
calendars will correspond to an actual person, `names` will contain the list of
names for each url. Make sure you spefify the same number of urls and names.
The `username` and `password` fields are self-explaining: include the user and
password to access the calendars.
If you want to use a different type of calendar, such as Google Calendar, you
will need to create a new driver. Patches are welcome :).
## Running
The scripts directory contains a simple launcher script using a virtual environment,
and a systemd unit file you can use to ensure the program runs on startup.
When the application is stopped, it should clear the e-ink display, which is a good
idea to avoid displa burnout. In some cases, it may not happen (for example if
you get a power disruption). You can use the `reset_eink.py` script in those
cases to clear the screen.
## Usage
When started, the calendar will show today's calendar for everyone, from 8:00 to
21:00 (times are not configurable at the moment). You can switch to a weekly, Monday-to-Friday
calendar for each person by pressing the 2, 3 or 4 buttons in the keypad, and
switch back to the daily calendar by pressing 1. Yes, that means that having
more than 3 calendars could require code changes ;).
After 9 PM, the calendar will enter in screensaver mode, and display the image
in the `img/night_image.jpg` file. The image will be converted to a 1-bit format,
so it's better if you use a 1-bit image already.
If there is a JPG or PNG file named after today's date, in DD-MM-YYYY format
(for example, 01-01-2022.png for January 1st, 2022), the screensaver will use that
image instead of the default one. Be creative!
## License
Refer to the LICENSE file for licensing details.
The weathericons-regular-webfont font is licensed under the [SIL OFL 1.1](http://scripts.sil.org/OFL)
license.
The [DejaVuSansMono-Bold font](https://dejavu-fonts.github.io/) is licensed under the
Bitstream Vera and Public Domain.
The `epd7in5bc.py` and `epdconfig.py` files are taken from the [Waveshare e-Paper repository](https://github.com/waveshare/e-Paper/),
including patches inspired by [this pull request](https://github.com/waveshare/e-Paper/pull/104)
to improve performance.
## Anything missing?
Feel free to contact me. This project was mainly set to scratch a personal itch,
but if it can be helpful to anyone, I'd be more than happy to improve it and
its documentation.
## Author
Javier Peña (@fj_pena).

20
config.ini Normal file
View File

@@ -0,0 +1,20 @@
[DEFAULT]
# Valid video drivers: pygame (for testing), eink for e-paper screen
video = pygame
# Valid keyboard drivers: pygame (for testing), gpio for 1x4 matrix keypad connected to GPIO pins
keyboard = pygame
[weather]
# OpenWeatherMap API key and location
# See https://openweathermap.org/appid
owm_api_key = <insert API key here>
# For the location code, search for your location in https://openweathermap.org/,
# then use the id in the resulting URL, for example https://openweathermap.org/city/3129046
owm_location = <insert location id here>
[calendar]
# URL of all caldav calendars, separated by commas
urls = http://example.com/caldavcal1, http://example.com/caldavcal2, http://example.com/caldavcal3
names = user1, user2, user3
username = user
password = password

38
drivers/caldavprovider.py Normal file
View File

@@ -0,0 +1,38 @@
from datetime import datetime
import caldav
import icalendar
import pytz
import urllib3
urllib3.disable_warnings()
class CalDavProvider():
def __init__(self, username, password):
self.tz = pytz.timezone('Europe/Madrid')
self.username = username
self.password = password
def get_calendar(self, url, date_start, date_end):
# print("%s %s" % (date_start, date_end))
client = caldav.DAVClient(url=url, username=self.username, password=self.password, ssl_verify_cert=False)
calendar = caldav.Calendar(client=client, url=url)
returned_events = []
events_found = calendar.date_search(
start=date_start, end=date_end,
compfilter='VEVENT', expand=True)
if events_found:
for event in events_found:
cal = icalendar.Calendar.from_ical(event.data)
single_event = {}
for event in cal.walk('vevent'):
date_start = event.get('dtstart')
duration = event.get('duration')
summary = event.get('summary')
single_event['event_start'] = date_start.dt.astimezone(self.tz)
single_event['event_end'] = (date_start.dt + duration.dt).astimezone(self.tz)
single_event['event_title'] = summary
returned_events.append(single_event)
return returned_events

25
drivers/einkdriver.py Normal file
View File

@@ -0,0 +1,25 @@
from . import epd7in5bc
class EinkDriver():
def __init__(self, xres, yres):
self.epd = epd7in5bc.EPD()
self.epd.init()
self.epd.Clear()
self.xres = xres
self.yres = yres
# image1: black/white image
# image2: black/yellow image
def display(self, image1=None, image2=None):
self.epd.init()
self.epd.display(self.epd.getbuffer(image1), self.epd.getbuffer(image2))
self.epd.sleep()
def end(self):
print("Ending e-ink driver")
self.epd.init()
self.epd.Clear()
self.epd.sleep()
self.epd.Dev_exit()
print("e-ink driver finished")

197
drivers/epd7in5bc.py Normal file
View File

@@ -0,0 +1,197 @@
# *****************************************************************************
# * | File : epd7in5bc.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | Info :
# *----------------
# * | This version: V4.0
# * | Date : 2019-06-20
# # | Info : python demo
# -----------------------------------------------------------------------------
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import logging
from . import epdconfig
# Display resolution
EPD_WIDTH = 640
EPD_HEIGHT = 384
class EPD:
def __init__(self):
self.reset_pin = epdconfig.RST_PIN
self.dc_pin = epdconfig.DC_PIN
self.busy_pin = epdconfig.BUSY_PIN
self.cs_pin = epdconfig.CS_PIN
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(10)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([command])
epdconfig.digital_write(self.cs_pin, 1)
def send_data(self, data):
epdconfig.digital_write(self.dc_pin, 1)
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def send_data_array(self, data):
epdconfig.digital_write(self.dc_pin, 1)
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.SPI.writebytes2(data)
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
while(epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
epdconfig.delay_ms(100)
logging.debug("e-Paper busy release")
def init(self):
if (epdconfig.module_init() != 0):
return -1
self.reset()
self.send_command(0x01) # POWER_SETTING
self.send_data(0x37)
self.send_data(0x00)
self.send_command(0x00) # PANEL_SETTING
self.send_data(0xCF)
self.send_data(0x08)
self.send_command(0x30) # PLL_CONTROL
self.send_data(0x3A) # PLL: 0-15:0x3C, 15+:0x3A
self.send_command(0x82) # VCM_DC_SETTING
self.send_data(0x28) #all temperature range
self.send_command(0x06) # BOOSTER_SOFT_START
self.send_data(0xc7)
self.send_data(0xcc)
self.send_data(0x15)
self.send_command(0x50) # VCOM AND DATA INTERVAL SETTING
self.send_data(0x77)
self.send_command(0x60) # TCON_SETTING
self.send_data(0x22)
self.send_command(0x65) # FLASH CONTROL
self.send_data(0x00)
self.send_command(0x61) # TCON_RESOLUTION
self.send_data(self.width >> 8) # source 640
self.send_data(self.width & 0xff)
self.send_data(self.height >> 8) # gate 384
self.send_data(self.height & 0xff)
self.send_command(0xe5) # FLASH MODE
self.send_data(0x03)
return 0
def getbuffer(self, image):
img = image
imwidth, imheight = img.size
if(imwidth == self.width and imheight == self.height):
img = img.convert('1')
elif(imwidth == self.height and imheight == self.width):
img = img.rotate(90, expand=True).convert('1')
else:
logging.warning("Wrong image dimensions: must be " + str(self.width) + "x" + str(self.height))
# return a blank buffer
return [0x00] * (int(self.width/8) * self.height)
buf = bytearray(img.tobytes('raw'))
return buf
def display(self, imageblack, imagered):
buf = [0x00] * ((self.width // 2) * self.height)
for i in range(0, int(self.width / 8 * self.height)):
temp1 = imageblack[i]
temp2 = imagered[i]
j = 0
while (j < 8):
if ((temp2 & 0x80) == 0x00):
temp3 = 0x04 #red
elif ((temp1 & 0x80) == 0x00):
temp3 = 0x00 #black
else:
temp3 = 0x03 #white
temp3 = (temp3 << 4) & 0xFF
temp1 = (temp1 << 1) & 0xFF
temp2 = (temp2 << 1) & 0xFF
j += 1
if((temp2 & 0x80) == 0x00):
temp3 |= 0x04 #red
elif ((temp1 & 0x80) == 0x00):
temp3 |= 0x00 #black
else:
temp3 |= 0x03 #white
temp1 = (temp1 << 1) & 0xFF
temp2 = (temp2 << 1) & 0xFF
buf[i * 4 + (j//2)] = temp3
j += 1
self.send_command(0x10)
self.send_data_array(buf)
self.send_command(0x04) # POWER ON
self.ReadBusy()
self.send_command(0x12) # display refresh
epdconfig.delay_ms(100)
self.ReadBusy()
def Clear(self):
buf = [0x33] * ((self.width // 2) * self.height)
self.send_command(0x10)
self.send_data_array(buf)
self.send_command(0x04) # POWER ON
self.ReadBusy()
self.send_command(0x12) # display refresh
epdconfig.delay_ms(100)
self.ReadBusy()
def sleep(self):
self.send_command(0x02) # POWER_OFF
self.ReadBusy()
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0XA5)
def Dev_exit(self):
epdconfig.module_exit()
### END OF FILE ###

156
drivers/epdconfig.py Normal file
View File

@@ -0,0 +1,156 @@
# /*****************************************************************************
# * | File : epdconfig.py
# * | Author : Waveshare team
# * | Function : Hardware underlying interface
# * | Info :
# *----------------
# * | This version: V1.0
# * | Date : 2019-06-21
# * | Info :
# ******************************************************************************
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import os
import logging
import sys
import time
class RaspberryPi:
# Pin definition
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
def __init__(self):
import spidev
import RPi.GPIO
self.GPIO = RPi.GPIO
# SPI device, bus = 0, device = 0
self.SPI = spidev.SpiDev(0, 0)
def digital_write(self, pin, value):
self.GPIO.output(pin, value)
def digital_read(self, pin):
return self.GPIO.input(pin)
def delay_ms(self, delaytime):
time.sleep(delaytime / 1000.0)
def spi_writebyte(self, data):
self.SPI.writebytes(data)
def module_init(self):
self.GPIO.setmode(self.GPIO.BCM)
self.GPIO.setwarnings(False)
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
self.SPI.max_speed_hz = 4000000
self.SPI.mode = 0b00
return 0
def module_exit(self):
logging.debug("spi end")
self.SPI.close()
logging.debug("close 5V, Module enters 0 power consumption ...")
self.GPIO.setmode(self.GPIO.BCM)
self.GPIO.output(self.RST_PIN, 0)
self.GPIO.output(self.DC_PIN, 0)
self.GPIO.cleanup()
class JetsonNano:
# Pin definition
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
def __init__(self):
import ctypes
find_dirs = [
os.path.dirname(os.path.realpath(__file__)),
'/usr/local/lib',
'/usr/lib',
]
self.SPI = None
for find_dir in find_dirs:
so_filename = os.path.join(find_dir, 'sysfs_software_spi.so')
if os.path.exists(so_filename):
self.SPI = ctypes.cdll.LoadLibrary(so_filename)
break
if self.SPI is None:
raise RuntimeError('Cannot find sysfs_software_spi.so')
import Jetson.GPIO
self.GPIO = Jetson.GPIO
def digital_write(self, pin, value):
self.GPIO.output(pin, value)
def digital_read(self, pin):
return self.GPIO.input(self.BUSY_PIN)
def delay_ms(self, delaytime):
time.sleep(delaytime / 1000.0)
def spi_writebyte(self, data):
self.SPI.SYSFS_software_spi_transfer(data[0])
def module_init(self):
self.GPIO.setmode(self.GPIO.BCM)
self.GPIO.setwarnings(False)
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
self.SPI.SYSFS_software_spi_begin()
return 0
def module_exit(self):
logging.debug("spi end")
self.SPI.SYSFS_software_spi_end()
logging.debug("close 5V, Module enters 0 power consumption ...")
self.GPIO.output(self.RST_PIN, 0)
self.GPIO.output(self.DC_PIN, 0)
self.GPIO.cleanup()
if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'):
implementation = RaspberryPi()
else:
implementation = JetsonNano()
for func in [x for x in dir(implementation) if not x.startswith('_')]:
setattr(sys.modules[__name__], func, getattr(implementation, func))
### END OF FILE ###

52
drivers/gpiodriver.py Normal file
View File

@@ -0,0 +1,52 @@
from datetime import datetime
import RPi.GPIO as GPIO
import time
row_channels = [6, 5, 19, 13]
column_channel = 26
class GPIODriver():
def __init__(self):
GPIO.setmode(GPIO.BCM)
GPIO.setup(row_channels, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(column_channel, GPIO.OUT)
def wait_for_keypress(self, timeout=60):
self.pressed = None
def callback_1(channel):
# print("Pressed key 1")
self.pressed = 1
def callback_2(channel):
# print("Pressed key 2")
self.pressed = 2
def callback_3(channel):
# print("Pressed key 3")
self.pressed = 3
def callback_4(channel):
# print("Pressed key 4")
self.pressed = 4
GPIO.add_event_detect(row_channels[0], GPIO.RISING, callback=callback_1, bouncetime=500)
GPIO.add_event_detect(row_channels[1], GPIO.RISING, callback=callback_2, bouncetime=500)
GPIO.add_event_detect(row_channels[2], GPIO.RISING, callback=callback_3, bouncetime=500)
GPIO.add_event_detect(row_channels[3], GPIO.RISING, callback=callback_4, bouncetime=500)
GPIO.output(column_channel, GPIO.HIGH)
start_time = datetime.now()
while not self.pressed:
time.sleep(1)
current_time = datetime.now()
delta = current_time - start_time
if delta.seconds > timeout:
for chan in row_channels:
GPIO.remove_event_detect(chan)
GPIO.output(column_channel, GPIO.LOW)
return None
for chan in row_channels:
GPIO.remove_event_detect(chan)
GPIO.output(column_channel, GPIO.LOW)
return self.pressed

61
drivers/pygamedriver.py Normal file
View File

@@ -0,0 +1,61 @@
from datetime import datetime
import pygame
import sys
class PygameDriver():
def __init__(self, xres, yres):
pygame.init()
self.screen = pygame.display.set_mode((xres, yres))
self.xres = xres
self.yres = yres
# image1: black/white image
# image2: black/yellow image
def display(self, image1=None, image2=None):
if image1:
raw_str1 = image1.convert('RGBA').tobytes("raw", 'RGBA')
pygame_surface1 = pygame.image.fromstring(raw_str1, (self.xres, self.yres), 'RGBA')
self.screen.blit(pygame_surface1, (0, 0))
if image2:
raw_str2 = image2.convert('RGB').tobytes("raw", 'RGB')
pygame_surface2 = pygame.image.fromstring(raw_str2, (self.xres, self.yres), 'RGB')
pygame_surface2.set_colorkey((255, 255, 255))
image_pixel_array = pygame.PixelArray(pygame_surface2)
image_pixel_array.replace((0, 0, 0), (127, 100, 0))
del image_pixel_array
self.screen.blit(pygame_surface2, (0, 0))
pygame.display.flip()
def end(self):
pygame.quit()
class PygameKBDriver():
def __init__(self):
pass
def wait_for_keypress(self, timeout=60):
start_time = datetime.now()
while True:
current_time = datetime.now()
delta = current_time - start_time
if delta.seconds > timeout:
return None
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
pygame.quit()
sys.exit(0)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_1:
return 1
elif event.key == pygame.K_2:
return 2
elif event.key == pygame.K_3:
return 3
elif event.key == pygame.K_4:
return 4
pygame.time.wait(1000)

Binary file not shown.

View File

@@ -0,0 +1,187 @@
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
Bitstream Vera Fonts Copyright
------------------------------
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute the
Font Software, including without limitation the rights to use, copy, merge,
publish, distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to the
following conditions:
The above copyright and trademark notices and this permission notice shall
be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional glyphs or characters may be added to the Fonts, only if the fonts
are renamed to names not containing either the words "Bitstream" or the word
"Vera".
This License becomes null and void to the extent applicable to Fonts or Font
Software that has been modified and is distributed under the "Bitstream
Vera" names.
The Font Software may be sold as part of a larger software package but no
copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font Software
without prior written authorization from the Gnome Foundation or Bitstream
Inc., respectively. For further information, contact: fonts at gnome dot
org.
Arev Fonts Copyright
------------------------------
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining
a copy of the fonts accompanying this license ("Fonts") and
associated documentation files (the "Font Software"), to reproduce
and distribute the modifications to the Bitstream Vera Font Software,
including without limitation the rights to use, copy, merge, publish,
distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to
the following conditions:
The above copyright and trademark notices and this permission notice
shall be included in all copies of one or more of the Font Software
typefaces.
The Font Software may be modified, altered, or added to, and in
particular the designs of glyphs or characters in the Fonts may be
modified and additional glyphs or characters may be added to the
Fonts, only if the fonts are renamed to names not containing either
the words "Tavmjong Bah" or the word "Arev".
This License becomes null and void to the extent applicable to Fonts
or Font Software that has been modified and is distributed under the
"Tavmjong Bah Arev" names.
The Font Software may be sold as part of a larger software package but
no copy of one or more of the Font Software typefaces may be sold by
itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the name of Tavmjong Bah shall not
be used in advertising or otherwise to promote the sale, use or other
dealings in this Font Software without prior written authorization
from Tavmjong Bah. For further information, contact: tavmjong @ free
. fr.
TeX Gyre DJV Math
-----------------
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski
(on behalf of TeX users groups) are in public domain.
Letters imported from Euler Fraktur from AMSfonts are (c) American
Mathematical Society (see below).
Bitstream Vera Fonts Copyright
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera
is a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license (“Fonts”) and associated
documentation
files (the “Font Software”), to reproduce and distribute the Font Software,
including without limitation the rights to use, copy, merge, publish,
distribute,
and/or sell copies of the Font Software, and to permit persons to whom
the Font Software is furnished to do so, subject to the following
conditions:
The above copyright and trademark notices and this permission notice
shall be
included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional
glyphs or characters may be added to the Fonts, only if the fonts are
renamed
to names not containing either the words “Bitstream” or the word “Vera”.
This License becomes null and void to the extent applicable to Fonts or
Font Software
that has been modified and is distributed under the “Bitstream Vera”
names.
The Font Software may be sold as part of a larger software package but
no copy
of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL,
SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN
ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
INABILITY TO USE
THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the names of GNOME, the GNOME
Foundation,
and Bitstream Inc., shall not be used in advertising or otherwise to promote
the sale, use or other dealings in this Font Software without prior written
authorization from the GNOME Foundation or Bitstream Inc., respectively.
For further information, contact: fonts at gnome dot org.
AMSFonts (v. 2.2) copyright
The PostScript Type 1 implementation of the AMSFonts produced by and
previously distributed by Blue Sky Research and Y&Y, Inc. are now freely
available for general use. This has been accomplished through the
cooperation
of a consortium of scientific publishers with Blue Sky Research and Y&Y.
Members of this consortium include:
Elsevier Science IBM Corporation Society for Industrial and Applied
Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS)
In order to assure the authenticity of these fonts, copyright will be
held by
the American Mathematical Society. This is not meant to restrict in any way
the legitimate use of the fonts, such as (but not limited to) electronic
distribution of documents containing these fonts, inclusion of these fonts
into other public domain or commercial font collections or computer
applications, use of the outline data to create derivative fonts and/or
faces, etc. However, the AMS does require that the AMS copyright notice be
removed from any derivative versions of the fonts which have been altered in
any way. In addition, to ensure the fidelity of TeX documents using Computer
Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces,
has requested that any alterations which yield different font metrics be
given a different name.
$Id$

View File

@@ -0,0 +1,3 @@
Weather Icons licensed under SIL OFL 1.1: http://scripts.sil.org/OFL.
You can get the fonts at https://github.com/erikflowers/weather-icons

Binary file not shown.

BIN
img/night_image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

152
mycalendar.py Executable file
View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python
# Inspiration from https://github.com/zli117/EInk-Calendar/tree/master/resources
import configparser
import os
from datetime import datetime, timedelta
from PIL import Image, ImageFont, ImageDraw
import sys
import time
from drivers.caldavprovider import CalDavProvider
from widgets.calendarwidget import CalendarWidget
from widgets.timewidget import TimeWidget
from widgets.weatherwidget import WeatherWidget
def today_calendar():
fullimg = Image.new('1', (640, 384), color=255)
fullimg2 = Image.new('1', (640, 384), color=255)
weather = WeatherWidget(owm_api_key)
fullimg.paste(weather.get_weather(owm_location), box=(0, 0))
timewidget = TimeWidget()
fullimg.paste(timewidget.get_time(), box=(128, 0))
calendar = CalendarWidget(family_names)
event_getter = CalDavProvider(calendar_username, calendar_password)
test_event_list = []
today_start = datetime.now().replace(hour=8, minute=0, second=0, microsecond=0)
today_end = today_start.replace(hour=21, minute=0, second=0, microsecond=0)
for url in calendar_list:
test_event_list.append(event_getter.get_calendar(url, today_start, today_end))
img1, img2 = calendar.get_calendar(test_event_list)
fullimg.paste(img1, box=(0, 80))
fullimg2.paste(img2, box=(0, 80))
return fullimg, fullimg2
def week_calendar(calid, name):
fullimg = Image.new('1', (640, 384), color=255)
fullimg2 = Image.new('1', (640, 384), color=255)
weather = WeatherWidget(owm_api_key)
fullimg.paste(weather.get_weather(owm_location), box=(0, 0))
timewidget = TimeWidget()
fullimg.paste(timewidget.get_time(), box=(128, 0))
event_getter = CalDavProvider(calendar_username, calendar_password)
test_event_list = []
today = datetime.today()
curdate = today + timedelta(days=-today.weekday(), weeks=0)
curdate = curdate.replace(hour=8, minute=0, second=0, microsecond=0)
for day in range(7, 12):
test_event_list.append(event_getter.get_calendar(calendar_list[calid], curdate, curdate.replace(hour=21)))
curdate = curdate + timedelta(days=1)
calendar = CalendarWidget(['L', 'M', 'X', 'J', 'V'])
img1, img2 = calendar.get_calendar(test_event_list, title=name)
fullimg.paste(img1, box=(0, 80))
fullimg2.paste(img2, box=(0, 80))
return fullimg, fullimg2
def special_image():
today = datetime.now()
filename = '%02d-%02d-%4d' % (today.day, today.month, today.year)
filename_jpg = filename + '.jpg'
filename_png = filename + '.png'
img = None
img2 = Image.new('1', (640, 384), color=255)
if os.path.exists(os.path.join('./img', filename_jpg)):
img = Image.open(os.path.join('./img', filename_jpg)).convert('1').resize((640,384))
elif os.path.exists(os.path.join('./img', filename_png)):
img = Image.open(os.path.join('./img', filename_png)).convert('1').resize((640,384))
if img:
return img, img2, True
return img2, img2, False
def night_image():
img = Image.open(os.path.join('./img', 'night_image.jpg')).convert('1').resize((640,384))
img2 = Image.new('1', (640, 384), color=255)
return img, img2
cp = configparser.RawConfigParser()
cp.read('config.ini')
video_driver = cp.get('DEFAULT', 'video')
kbd_driver = cp.get('DEFAULT', 'keyboard')
owm_api_key = cp.get('weather', 'owm_api_key')
owm_location = cp.getint('weather', 'owm_location')
calendar_username = cp.get('calendar', 'username')
calendar_password = cp.get('calendar', 'password')
calendar_list=[]
for item in cp.get('calendar', 'urls').split(','):
calendar_list.append(item.strip())
family_names = []
for item in cp.get('calendar', 'names').split(','):
family_names.append(item.strip())
if video_driver == 'pygame':
from drivers.pygamedriver import PygameDriver
video = PygameDriver(640, 384)
elif video_driver == 'eink':
from drivers.einkdriver import EinkDriver
video = EinkDriver(640, 384)
if kbd_driver == 'pygame':
from drivers.pygamedriver import PygameKBDriver
keyboard = PygameKBDriver()
elif kbd_driver == 'gpio':
from drivers.gpiodriver import GPIODriver
keyboard = GPIODriver()
current_screen = 0
try:
while True:
print("Reading calendar data", flush=True)
start_time = datetime.now()
images = []
images.append(today_calendar())
images.append(week_calendar(0, family_names[0]))
images.append(week_calendar(1, family_names[1]))
images.append(week_calendar(2, family_names[2]))
special_img1, special_img2, is_today_special = special_image()
if not is_today_special:
special_img1, special_img2 = night_image()
images.append([special_img1, special_img2])
done = False
refresh = True
while not done:
if refresh:
print("Display", flush=True)
video.display(image1=images[current_screen][0], image2=images[current_screen][1])
refresh = False
print("Wait for keypress", flush=True)
value = keyboard.wait_for_keypress(timeout=60)
print("Received keypress: %s" % value, flush=True)
if value:
current_screen = value - 1
refresh = True
else:
current_time = datetime.now()
if current_time.hour > 20 or current_time.hour < 7:
current_screen = 4
else:
current_screen = 0
current_time = datetime.now()
delta_time = current_time - start_time
if delta_time.seconds > 600:
# After 10 minutes, re-read calendar data
done = True
finally:
print("Quit", flush=True)
video.end()

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
caldav
icalendar
pygame
pillow
pyowm
pytz

5
reset_eink.py Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env python
from drivers.einkdriver import EinkDriver
video = EinkDriver(640, 384)
video.end()

14
scripts/calendar.service Normal file
View File

@@ -0,0 +1,14 @@
[Unit]
Description=My cool e-ink family calendar
After=syslog.target network.target
[Service]
Type=simple
User=pi
ExecStart=/home/pi/launcher.sh
PrivateTmp=false
KillMode=control-group
Restart=always
[Install]
WantedBy=multi-user.target

5
scripts/launcher.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
cd ~/calendario
source .venv/bin/activate
./mycalendar.py

81
widgets/calendarwidget.py Normal file
View File

@@ -0,0 +1,81 @@
import os
from datetime import datetime
from pyowm import OWM
from PIL import Image, ImageFont, ImageDraw
def get_local_ip():
import socket
"""Try to determine the local IP address of the machine."""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Use Google Public DNS server to determine own IP
sock.connect(('192.168.1.1', 80))
return sock.getsockname()[0]
except socket.error:
try:
return socket.gethostbyname(socket.gethostname())
except socket.gaierror:
return '127.0.0.1'
finally:
sock.close()
class CalendarWidget():
def __init__(self, column_names):
self.column_names = column_names
self.font14 = ImageFont.truetype(os.path.join('./fonts/', 'DejaVuSansMono-Bold.ttf'), 14)
def create_entry_box(self, inittime, finishtime, text, width):
"""
both inittime and finishtime are objects of type datetime.datetime
"""
timediff = finishtime - inittime
hours = timediff.seconds / 3600
img = Image.new('1', (width, int(hours * 20)), color=0)
imgdraw = ImageDraw.Draw(img)
imgdraw.text((8, 0), text, font=self.font14, fill=255)
return img
def get_calendar(self, column_events, title=None):
"""
column_events contains a list of columns, and each of them is a list of hashes containing:
- event_start: start time
- event_end: ending time
- event_title: event title
"""
img = Image.new('1', (640, 304), color=255)
img2 = Image.new('1', (640, 304), color=255)
imgdraw = ImageDraw.Draw(img)
imgdraw2 = ImageDraw.Draw(img2)
index = 0
distance = 540 // len(self.column_names)
boxwidth = 570 // len(self.column_names)
for column in self.column_names:
imgdraw.text((128 + distance*index, 0), column, font=self.font14, fill=0)
index += 1
if title:
imgdraw.text((8, 0), title, font=self.font14, fill=0)
imgdraw.text((540, 294), get_local_ip())
# Draw Calendar lines
for i in range(0, 14):
imgdraw2.line([(0,30 + 20*i), (639, 30 + 20*i)], fill=0, width=1)
for i in range(0, 13):
imgdraw.text((0, 30 + 20*i), '%2d:00' % (i+8), font=self.font14, fill=0)
column_number = 0
for column_list in column_events:
for event in column_list:
if event['event_start'].hour >= 8 or event['event_start'].hour <= 19:
eventimg = self.create_entry_box(event['event_start'],
event['event_end'],
event['event_title'],
boxwidth)
x = column_number + 64 + (boxwidth * column_number)
y = 30 + int(20 * (event['event_start'].hour + (event['event_start'].minute / 60) - 8.0))
img2.paste(eventimg, box = (x, y))
column_number += 1
return img, img2

17
widgets/timewidget.py Normal file
View File

@@ -0,0 +1,17 @@
import os
from datetime import datetime
from PIL import Image, ImageFont, ImageDraw
class TimeWidget():
def __init__(self):
self.font = ImageFont.truetype(os.path.join('./fonts/', 'DejaVuSansMono-Bold.ttf'), 24)
def get_time(self):
now = datetime.now()
img = Image.new('1', (512, 64), color=255)
imgdraw = ImageDraw.Draw(img)
imgdraw.text((32, 24), '%s, %2d/%2d/%4d' % (self.weekdays[now.isoweekday()], now.day, now.month, now.year), font=self.font, fill=0)
imgdraw.text((400, 24), '%02d:%02d' % (now.hour, now.minute), font=self.font, fill=0)
return img
weekdays = ['None', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo']

87
widgets/weatherwidget.py Normal file
View File

@@ -0,0 +1,87 @@
import os
from pyowm import OWM
from PIL import Image, ImageFont, ImageDraw
class WeatherWidget():
def __init__(self, api_key):
self.api_key = api_key
self.weatherfont = ImageFont.truetype(os.path.join('./fonts/', 'weathericons-regular-webfont.ttf'), 48)
self.font16 = ImageFont.truetype(os.path.join('./fonts/', 'DejaVuSansMono-Bold.ttf'), 16)
self.owm = OWM(api_key)
def get_weather(self, location):
mgr = self.owm.weather_manager()
current = mgr.weather_at_id(location)
w = current.weather
temp = w.temperature('celsius')['temp']
wcode = w.weather_code
img = Image.new('1', (128, 64), color=255)
imgdraw = ImageDraw.Draw(img)
imgdraw.text((0, 0), self.wcode_to_unicode[wcode], font=self.weatherfont, fill=0)
imgdraw.text((64, 32), '%.1fº' % temp, font=self.font16, fill=0)
return img
wcode_to_unicode = {
200: u'\uf01e',
201: u'\uf01e',
202: u'\uf01e',
210: u'\uf016',
211: u'\uf016',
212: u'\uf016',
221: u'\uf016',
230: u'\uf01e',
231: u'\uf01e',
232: u'\uf01e',
300: u'\uf01c',
301: u'\uf01c',
302: u'\uf019',
310: u'\uf017',
311: u'\uf019',
312: u'\uf019',
313: u'\uf01a',
314: u'\uf019',
321: u'\uf01c',
500: u'\uf01c',
501: u'\uf019',
502: u'\uf019',
503: u'\uf019',
504: u'\uf019',
511: u'\uf017',
520: u'\uf01a',
521: u'\uf01a',
522: u'\uf01a',
531: u'\uf01d',
600: u'\uf01b',
601: u'\uf01b',
602: u'\uf0b5',
611: u'\uf017',
612: u'\uf017',
615: u'\uf017',
616: u'\uf017',
620: u'\uf017',
621: u'\uf01b',
622: u'\uf01b',
701: u'\uf014',
711: u'\uf062',
721: u'\uf0b6',
731: u'\uf063',
741: u'\uf014',
761: u'\uf063',
762: u'\uf063',
771: u'\uf011',
781: u'\uf056',
800: u'\uf00d',
801: u'\uf011',
802: u'\uf011',
803: u'\uf012',
804: u'\uf013',
900: u'\uf056',
901: u'\uf01d',
902: u'\uf073',
903: u'\uf076',
904: u'\uf072',
905: u'\uf021',
906: u'\uf015',
957: u'\uf050',
}