Added RSS feeds and 3rd article [WIP]

This commit is contained in:
Fabio Manganiello 2021-01-26 01:49:49 +01:00
parent d89725f8ab
commit 60ab950093
11 changed files with 406 additions and 141 deletions

View File

@ -5,7 +5,7 @@ import re
from glob import glob
from typing import Optional
from flask import Flask, abort, send_from_directory, render_template
from flask import Flask, Response, abort, send_from_directory, render_template
from markdown import markdown
basedir = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..'))
@ -63,13 +63,14 @@ def get_page(page: str, title: Optional[str] = None):
description=metadata.get('description'),
published=(metadata['published'].strftime('%b %d, %Y')
if metadata.get('published') else None),
content=markdown(f.read(),extensions=['fenced_code', 'codehilite']))
content=markdown(f.read(), extensions=['fenced_code', 'codehilite']))
def get_pages() -> list:
def get_pages(with_content: bool = False) -> list:
return sorted([
{
'path': path,
'content': get_page(path) if with_content else '',
**get_page_metadata(os.path.basename(path)),
}
for path in glob(os.path.join(pages_dir, '*.md'))
@ -99,3 +100,50 @@ def css_route(style: str):
@app.route('/article/<article>', methods=['GET'])
def article_route(article: str):
return get_page(article)
@app.route('/rss', methods=['GET'])
def rss_route():
pages = get_pages(with_content=True)
return Response('''<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Platypush blog feeds</title>
<link>http://blog.platypush.tech</link>
<description>Insights and inspirational projects using Platypush as an automation platform</description>
<category>Programming, automation, Python, machine learning, IoT, smart home</category>
<image>
<url>https://git.platypush.tech/uploads/-/system/appearance/header_logo/1/icon-256.png</url>
<title>Platypush</title>
<link>https://git.platypush.tech</link>
</image>
<pubDate>{last_pub_date}</pubDate>
<language>en-us</language>
{items}
</channel>
</rss>'''.format(
last_pub_date=pages[0]['published'].strftime('%a, %d %b %Y %H:%M:%S GMT'),
items='\n\n'.join([
'''
<item>
<title>{title}</title>
<link>https://blog.platypush.tech{link}</link>
<pubDate>{published}</pubDate>
<description><![CDATA[{content}]]></description>
<image>
<url>https://blog.platypush.tech/img{image}</url>
</image>
</item>
'''.format(
title=page.get('title', '[No Title]'),
link=page.get('uri', ''),
published=page['published'].strftime('%a, %d %b %Y %H:%M:%S GMT') if 'published' in page else '',
# description=page.get('description', ''),
content=page.get('content', ''),
image=page.get('image', ''),
)
for page in pages
]),
), mimetype='application/rss+xml')

View File

@ -1,9 +1,15 @@
main .content {
display: flex;
flex-direction: column;
text-align: justify;
line-height: 1.5em;
letter-spacing: .04em;
}
main .content p,
main .content ul {
font-family: Avenir, Palatino, charter, Georgia, Cambria, "Times New Roman", Times, serif;
text-align: justify;
overflow-wrap: break-word;
word-break: break-word;
}
main .content code, .codehilite {

View File

@ -1,74 +1,97 @@
html {
font-size: calc(1em + 1vw);
}
@media screen and (min-width: 1024px) {
html {
font-size: 20px;
}
height: 100%;
font-size: 20px;
font-family: BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, Helvetica, Arial, sans-serif;
font-weight: 400;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
width: 100%;
height: 100%;
margin: 0;
width: 100%;
height: 100%;
}
a, a:visited {
color: #555;
border-bottom: 1px dashed #999;
text-decoration: none;
color: #555;
border-bottom: 1px dashed #999;
text-decoration: none;
}
header {
display: flex;
align-items: center;
height: 3em;
padding: 0 .5em;
box-shadow: 1px 3px 3px 0 #bbb;
display: flex;
align-items: center;
height: 8%;
padding: 0 .5em;
box-shadow: 1px 3px 3px 0 #bbb;
}
@media screen and (max-width: 767px) {
header {
height: 4em;
}
header a {
display: flex;
align-items: center;
border-bottom: none;
}
header > a {
display: flex;
align-items: center;
border-bottom: none;
header .left,
header .left a,
header .right {
display: inline-flex;
align-items: center;
width: 50%;
}
header .right {
display: inline-flex;
justify-content: right;
text-align: right;
opacity: .8;
}
header .icon {
background: url(/img/icon.png);
background-size: 40px;
width: 40px;
height: 40px;
display: inline-flex;
margin-right: 1em;
background-size: 40px !important;
width: 40px;
height: 40px;
display: inline-flex;
}
header .left .icon {
background: url(/img/icon.png);
margin-right: 1em;
}
header .right .icon {
background: url(/img/rss.png);
}
header .title {
display: inline-flex;
display: inline-flex;
}
main {
height: calc(100% - 3em);
overflow: auto;
display: flex;
flex-direction: column;
align-items: center;
font-family: Avenir, Palatino, Georgia, Verdana, Helvetica, Arial, sans-serif;
padding: 0 2em;
height: 92%;
overflow: auto;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 2em;
}
h1 {
font-size: 2em;
line-height: 1.2em;
font-size: 2em;
line-height: 1.2em;
}
h2 {
font-size: 1.5em;
line-height: 1.1em;
font-size: 1.5em;
line-height: 1.1em;
}
@media screen and (min-width: 768px) {
header {
height: 6%;
}
main {
height: 94%;
}
}

View File

@ -4,11 +4,16 @@ main {
.articles {
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
overflow: auto;
}
.article {
display: block;
width: 100%;
max-height: 80%;
box-shadow: 0 1px 3px 1px #ddd;
overflow: hidden;
text-overflow: ellipsis;
@ -17,33 +22,39 @@ main {
.article:hover {
box-shadow: 0 1px 4px 2px #bcbcbc;
}
}
@media screen and (max-width: 767px) {
@media screen and (min-width: 767px) {
.article {
width: 100%;
max-height: 65%;
}
}
@media screen and (min-width: 768px) and (max-width: 990px) {
@media screen and (min-width: 640px) and (max-width: 767px) {
.article {
padding: 0 calc(1em + 7vw);
}
}
@media screen and (min-width: 768px) and (max-width: 979px) {
.article {
width: 50%;
}
}
@media screen and (min-width: 990px) and (max-width: 1023px) {
@media screen and (min-width: 980px) and (max-width: 1279px) {
.article {
width: 33%;
}
}
@media screen and (min-width: 1024px) and (max-width: 1279px) {
@media screen and (min-width: 1280px) and (max-width: 1599px) {
.article {
width: 25%;
}
}
@media screen and (min-width: 1280px) {
@media screen and (min-width: 1600px) {
.article {
width: 20%;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
static/img/rss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,168 @@
[//]: # (title: Detect people with a RaspberryPi, a thermal camera, Platypush and Tensorflow)
[//]: # (description: Use cheap components and open-source software to build a robust presence detector.)
[//]: # (image: /img/people-detect-1.png)
[//]: # (published: 2019-09-27)
Triggering events based on the presence of people has been the dream of many geeks and DIY automation junkies for a
while. Having your house to turn the lights on or off when you enter or exit your living room is an interesting
application, for instance. Most of the solutions out there to solve these kinds of problems, even more high-end
solutions like the [Philips Hue sensors](https://www2.meethue.com/en-us/p/hue-motion-sensor/046677473389), detect
motion, not actual people presence — which means that the lights will switch off once you lay on your couch like a
sloth. The ability to turn off music and/or tv when you exit the room and head to your bedroom, without the hassle of
switching all the buttons off, is also an interesting corollary. Detecting the presence of people in your room while
youre not at home is another interesting application.
Thermal cameras coupled with deep neural networks are a much more robust strategy to actually detect the presence of
people. Unlike motion sensors, they will detect the presence of people even when they arent moving. And unlike optical
cameras, they detect bodies by measuring the heat that they emit in the form of infrared radiation, and are therefore
much more robust — their sensitivity doesnt depend on lighting conditions, on the position of the target, or the
colour. Before exploring the thermal camera solution, I tried for a while to build a model that instead relied on
optical images from a traditional webcam. The differences are staggering: I trained the optical model on more than ten
thousands 640x480 images taken all through a week in different lighting conditions, while I trained the thermal camera
model on a dataset of 900 24x32 images taken during a single day. Even with more complex network architectures, the
optical model wouldnt score above a 91% accuracy in detecting the presence of people, while the thermal model would
achieve around 99% accuracy within a single training phase of a simpler neural network. Despite the high potential,
theres not much out there in the market — theres been some research work on the topic (if you google “people detection
thermal camera” youll mostly find research papers) and a few high-end and expensive products for professional
surveillance. In lack of ready-to-go solutions for my house, I decided to take on my duty and build my own solution —
making sure that it can easily be replicated by anyone.
## Prepare the hardware
For this example we'll use the following hardware:
- A RaspberryPi (cost: around $35). In theory any model should work, but its probably not a good idea to use a
single-core RaspberryPi Zero for machine learning tasks — the task itself is not very expensive (well only use the
Raspberry for doing predictions on a trained model, not to train the model), but it may still suffer some latency on a
Zero. Plus, it may be really painful to install some of the required libraries (like Tensorflow or OpenCV) on
the `arm6` architecture used by the RaspberryPi Zero. Any better performing model (from RPi3 onwards) should
definitely do the job.
- A thermal camera. For this project, Ive used the
[MLX90640](https://shop.pimoroni.com/products/mlx90640-thermal-camera-breakout) Pimoroni breakout camera (cost: $55),
as its relatively cheap, easy to install, and it provides good results. This camera comes in standard (55°) and
wide-angle (110°) versions. Ive used the wide-angle model as the camera monitors a large living room, but take into
account that both have the same resolution (32x24 pixels), so the wider angle comes with the cost of a lower spatial
resolution. If you want to use a different thermal camera theres not much youll need to change, as long as it comes
with a software interface for RaspberryPi and
its [compatible with Platypush](https://platypush.readthedocs.io/en/latest/platypush/plugins/camera.ir.mlx90640.html).
Setting up the MLX90640 on your RaspberryPi if you have a Breakout Garden its easy as a pie. Fit the Breakout Garden on
top of your RaspberryPi. Fit the camera breakout into an I2C slot. Boot the RaspberryPi. Done. Otherwise, you can also
connect the device directly to the [RaspberryPi I2C interface](https://radiostud.io/howto-i2c-communication-rpi/),
either using the right hardware PINs or the software emulation layer.
## Prepare the software
I tested my code on Raspbian, but with a few minor modifications it should be easily adaptable to any distribution
installed on the RaspberryPi.
The software support for the thermal camera requires a bit of work. The MLX90640 doesnt come (yet) with a Python
ready-to-use interface, but a [C++ open-source driver is provided](https://github.com/pimoroni/mlx90640-library) -
and that's the driver that is wrapped by the Platypush integration. Instructions to install it:
```shell
# Install the dependencies
[sudo] apt-get install libi2c-dev
# Enable the I2C interface
echo dtparam=i2c_arm=on | sudo tee -a /boot/config.txt
# It's advised to configure the SPI bus baud rate to
# 400kHz to support the higher throughput of the sensor
echo dtparam=i2c1_baudrate=400000 | sudo tee -a /boot/config.txt
# A reboot is required here if you didn't have the
# options above enabled in your /boot/config.txt
[sudo] reboot
# Clone the driver's codebase
git clone https://github.com/pimoroni/mlx90640-library
cd mlx90640-library
# Compile the rawrgb example
make clean
make bcm2835
make I2C_MODE=LINUX examples/rawrgb
```
If it all went well you should see an executable named `rawrgb` under the `examples` directory. If you run it you should
see a bunch of binary data — thats the raw binary representation of the frames captured by the camera. Remember where
it is located or move it to a custom bin folder, as its the executable that platypush will use to interact with the
camera module.
This post assumes that you have already installed and configured Platypush on your system. If not, head to my post on
[getting started with Platypush](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush),
the [readthedocs page](https://platypush.readthedocs.io/en/latest/), the
[Gitlab page](https://git.platypush.tech/platypush/platypush) or
[the wiki](https://git.platypush.tech/platypush/platypush/-/wikis/home).
Install also the Python dependencies for the HTTP server, the MLX90640 plugin and Tensorflow:
```shell
[sudo] pip install 'platypush[http,tensorflow,mlx90640]'
```
Heading to your computer (we'll be using it for building the model that will be used on the RaspberryPi), install
OpenCV, Tensorflow and Jupyter and my utilities for handling images:
```shell
# For image manipulation
[sudo] pip install opencv
# Install Jupyter notebook to run the training code
[sudo] pip install jupyterlab
# Then follow the instructions at https://jupyter.org/install
# Tensorflow framework for machine learning and utilities
[sudo] pip install tensorflow numpy matplotlib
# Clone my repository with the image and training utilities
# and the Jupyter notebooks that we'll use for training.
git clone https://github.com/BlackLight/imgdetect-utils
```
## Capturing phase
Now that youve got all the hardware and software in place, its time to start capturing frames with your camera and use
them to train your model. First, configure
the [MLX90640 plugin](https://platypush.readthedocs.io/en/latest/platypush/plugins/camera.ir.mlx90640.html) in your
Platypush configuration file (by default, `~/.config/platypush/config.yaml`):
```yaml
# Enable the webserver
backend.http:
enabled: True
camera.ir.mlx90640:
fps: 16 # Frames per second
rotate: 270 # Can be 0, 90, 180, 270
rawrgb_path: /path/to/your/rawrgb
```
Restart the service, and if you haven't already create a user from the web interface at `http://your-rpi:8008`. You
should now be able to take pictures through the API:
```yaml
curl -XPOST -H 'Content-Type: application/json' -d '
{
"type":"request",
"action":"camera.ir.mlx90640.capture",
"args": {
"output_file":"~/snap.png",
"scale_factor":20
}
}' -a 'username:password' http://localhost:8008/execute
```
If everything went well, the thermal picture should be stored under `~/snap.png`. In my case it looks like this while
Im in standing front of the sensor:
![Thermal camera snapshot](../img/people-detect-1.png)
Notice the glow at the bottom-right corner — thats actually the heat from my RaspberryPi 4 CPU. Its there in all the
images I take, and you may probably see similar results if you mounted your camera on top of the Raspberry itself, but
it shouldnt be an issue for your model training purposes.
If you open the web panel (`http://your-host:8008`) youll also notice a new tab, represented by the sun icon, that you
can use to monitor your camera from a web interface.

View File

@ -1,41 +1,29 @@
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="/css/common.css">
<link rel="stylesheet" type="text/css" href="/css/blog.css">
<link rel="stylesheet" type="text/css" href="/css/code.css">
<title>{{ title }}</title>
</head>
{% with title=title or 'Platypush blog', styles=['/css/blog.css', '/css/code.css'] %}
{% include 'common-head.html' %}
{% endwith %}
<body>
<header>
<a href="/">
<div class="icon"></div>
<div class="title">PlatyBlog</div>
</a>
</header>
<main>
<div class="container">
<div class="title">
<h1>{{ title }}</h1>
</div>
{% if description %}
<div class="description">
<h3>{{ description }}</h3>
</div>
{% endif %}
{% if published %}
<div class="published-date">
Published on {{ published }}
</div>
{% endif %}
<div class="content">
{{ content | safe }}
</div>
<main>
<div class="container">
<div class="title">
<h1>{{ title }}</h1>
</div>
</main>
</body>
</html>
{% if description %}
<div class="description">
<h3>{{ description }}</h3>
</div>
{% endif %}
{% if published %}
<div class="published-date">
Published on {{ published }}
</div>
{% endif %}
<div class="content">
{{ content | safe }}
</div>
</div>
</main>
{% include 'common-tail.html' %}

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="alternate" type="application/rss+xml" title="RSS Feed for the Platypush blog" href="/rss" />
<link rel="stylesheet" type="text/css" href="/css/common.css">
{% if styles %}
{% for style in styles %}
<link rel="stylesheet" type="text/css" href="{{ style }}">
{% endfor %}
{% endif %}
<title>{{ title }}</title>
</head>
<body>
<header>
<div class="left">
<a href="/">
<div class="icon"></div>
<div class="title">PlatyBlog</div>
</a>
</div>
<div class="right">
<a href="/rss" target="_blank">
<div class="icon"></div>
</a>
</div>
</header>

View File

@ -0,0 +1,2 @@
</body>
</html>

View File

@ -1,48 +1,37 @@
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="/css/common.css">
<link rel="stylesheet" type="text/css" href="/css/home.css">
<title>Platypush blog</title>
</head>
{% with title=title or 'Platypush blog', styles=['/css/home.css'] %}
{% include 'common-head.html' %}
{% endwith %}
<body>
<header>
<a href="/">
<div class="icon"></div>
<div class="title">PlatyBlog</div>
</a>
</header>
<main>
<div class="articles">
{% for page in pages %}
<a class="article" href="{{ page['uri'] }}">
<div class="container">
{% if page['image'] %}
<div class="image">
<img src="{{ page['image'] }}" alt="">
</div>
{% endif %}
<div class="title">
{{ page['title'] }}
</div>
{% if page['published'] %}
<div class="published-date">
{{ page['published'].strftime('%b %d, %Y') }}
</div>
{% endif %}
{% if page['description'] %}
<div class="description">
{{ page['description'] }}
</div>
{% endif %}
<main>
<div class="articles">
{% for page in pages %}
<a class="article" href="{{ page['uri'] }}">
<div class="container">
{% if page['image'] %}
<div class="image">
<img src="{{ page['image'] }}" alt="">
</div>
</a>
{% endfor %}
</div>
</main>
</body>
</html>
{% endif %}
<div class="title">
{{ page['title'] }}
</div>
{% if page['published'] %}
<div class="published-date">
{{ page['published'].strftime('%b %d, %Y') }}
</div>
{% endif %}
{% if page['description'] %}
<div class="description">
{{ page['description'] }}
</div>
{% endif %}
</div>
</a>
{% endfor %}
</div>
</main>
{% include 'common-tail.html' %}