LocalPrinter can receive messages on Telegram, get news headlines, spotify playlist updates, weather forcasts, daily quotes and more. All fetching, processing and rendering is done on the device, no server needed.
By default it automatically prints an update issue every morning at 7am and a full issue when the button is pushed.
π¨ Pre Release: This project is under active development. It's likely to break at any time (or be broken right now).
Demo images here.
- Raspberri Pi Zero 2 W
- Cashino thermal printer
- 9V-5V step down (mine)
- Momentary button with LED (mine)
- 2.1mm DC panel mount (mine)
- 9V 2A power supply (mine)
- 4700uf capacitor (mine)
- Strip board, electrical wire & soldering iron
- Wooden box
- Enable the serial port
- Install
nodejs
curl -sL https://deb.nodesource.com/setup_16.x | sudo bash -
apt-get install -y nodejs
- Install
canvas
dependancies:sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
Clone the repository, create a config file according to the below and npm install
. You can also enable DEV_MODE
to skip printing and render straight to file for testing.
I also recommend installing Tailscale + SSH to make managing the printer remotely easier (expecially if you're making one for a friend).
Run with node index.js
.
You can keep the printer running and launch at boot by creating a service:
sudo nano /lib/systemd/system/localprinter.service
You may need to change the ExecStart
path below depending on where you cloned the repository.
[Unit]
Description=localprinter
Documentation=https://github.com/0x62/LocalPrinter
After=network.target
[Service]
Type=simple
User=pi
ExecStart=/usr/bin/node /home/pi/LocalPrinter/index.js
Restart=on-failure
[Install]
WantedBy=multi-user.target
Reload and start the service
sudo systemctl daemon-reload
sudo systemctl start localprinter
Hardware is heavily based on the Adafruit IoT printer, with the addition of a 9V power supply for the printer (and step down to 5V for Pi).
Connect the data port on the printer to the Pi GND, GPIO14 and GPIO15. Connect the printer power to 9V power supply. You can actually power the printer off 5V if you need to, but the print quality is much better with more power.
Connect the button to GPIOX, GPIOY, and the LED+ to GPIOZ (if applicable). A button with LED isn't required, but recomended as the LED will flash to indicate activity.
Copy the default .env.example
to .env
and configure the plugins as needed. Alternatively you can edit index.js
and configure the LocalPrinter directly.
# Required
# =========
# Serial port
SERIAL_PORT = "/dev/serial0"
# Delete rendered issue after printing
DELETE_AFTER_PRINT = true
# Set to true to disable printing and automatic delete (see `output/`)
DEV_MODE = false
# Issue header
ISSUE_TITLE_FULL = "THE DAILY 0x62"
ISSUE_TITLE_UPDATE = "YOU'VE GOT MAIL"
ISSUE_DATE_FMT = "MMMM Do YYYY, hA"
# Cron expression for when to automatically print issues
# You can add additional schedules in index.js
AUTO_ISSUE_SCHEDULE = "0 7 * * *" # Every day at 7am
# Type of automatic issue
# update = only print if there are updates
# full = print last update from all modules
AUTO_ISSUE_TYPE = "update"
# Type of issue to print when pushing button
BUTTON_TYPE = "full"
# Plugins (provide all options to enable)
# =======================================
# Monitor a playlist and print 3 new songs a day
SPOTIFY_CLIENT_ID = ""
SPOTIFY_SECRET = ""
SPOTIFY_PLAYLIST = "" # Playlist ID (public only)
# Instagram user to print each new post in daily update
# Public profiles only & endpoint is highly rate limited
# will probably break at some point
INSTAGAM_USER = ""
# Print messages received to Telegram bot
TELEGRAM_TOKEN = ""
TG_ALLOWED_IDS = "" # Comma seperated
TG_IMMEDIATE = true # Print received messages immediately, rather than in next update
# Headlines from The Guardian
GUARDIAN_TOKEN = ""
# Weather and rain/snow alerts
OPENWEATHER_TOKEN = ""
WEATHER_CITY = ""
# Daily quote
# Expects a CSV with quote, author (optional)
# Will randomly pick unseen options until all are used and reset
QUOTE_DB = "./data/quotes.csv"
QUOTE_IS_UPDATE = false # Set to true to still print this if no other providers have updates
There are two kinds of issue, full
and update
. Full issues print content from every module
regardless of whether there are updates (in this case it'll use the most recent data), and update
issues only include modules where there is an update.
There is also a special kind of issue used for real-time modules like the Telegram bot, which print
immediately after receiving an update. Unlike full
or update
, these issues do not increment the
issue number, and don't include the header/footer.
Monitor a Spotify playlist for changes, and include new songs in update issues. At most three new songs will be printed (oldest first). If more than three songs have been added since the last update the next three songs will be printed in the following update (allowing you to add many at a time without printing them all).
Monitor a Spotify playlist for changes, and include new songs in update issues. At most three new songs will be printed (oldest first). If more than three songs have been added since the last update the next three songs will be printed in the following update (allowing you to add many at a time without printing them all).
Prints out messages and images received to a Telegram bot. The first time you message the bot it
will reply with your user ID. Add this ID (and any friends) to TG_ALLOWED_IDS
to grant permission.
You can also disable immediate printing (and only include new messages in update/full issues) by
setting TG_IMMEDIATE = false
.
I'd like to add some other commands to this bot in future, e.g. printing out a custom message with the same renderer as the Quotes module (for massive poster-style text).
Prints the current conditions and hourly forecast for the next five hours.
Prints out your latest post the following morning. This module uses an undocumented Instagram API (instagram.com/username/?__a=1), which works well but is highly rate limited (a few calls an hour). Expect this module to break during development.
Print a daily motivation (or demotivational!) quote from a CSV database. This module could do with some TLC (probably should be connected to an API instead?).
I'd like to add these as core plugins but I haven't had time yet. Ideally for calendar and reminders
it would integrate with iOS (perhaps using a public calendar link and node-ical
?). Pull requests
welcome!
- Hacker News
- Product Hunt
- Spotify podcasts (recommendations and/or new episodes)
- Stocks, crypto and market activity
- Puzzles (cross words, sudoku etc)
- Calendar providers
- Todo/reminders providers
These are plugins I think would be cool, but are lower priority for me personally. Pull requests welcome!
- Random cocktail (https://www.thecocktaildb.com/api.php)
- Random facts
- Random jokes
You can create your own plugins and blocks, then load them printer.addPlugin(plugin, opts)
. Check
out index.js
for examples (used to load plugins based on env file).
Add a new folder to the plugins
directory that extends the core Plugin
class. Override the
fetch()
and render()
functions to fetch the data you need, and return an array of the blocks
used to render.
Create new blocks by extending the core Block
class, and provide a render()
function. Blocks
have access to the canvas context with this.ctx
, and the current Y position with this.startPosY
.
Your render function should return an object with one property, endPosY
, which is the new Y
position subsequent blocks should start at.
Core blocks like spacers, dividers, headers are exported as Blocks.Spacer
etc from core/index.js
.
Here is a very basic plugin which fetches some data from an API, checks if it's been seen before and prints it in updates:
import { Plugin, Blocks } from '../../core/index.js'
export default class MyAwesomePlugin extends Plugin {
constructor() {
super({
baseUrl: `https://myapi.com`,
})
}
get hasFreshContent() {
// Signal your plugin has fresh content (will be included in update issues)
return this.data.freshItems?.length > 0
}
get hasContent() {
// Signal your plugin has new or stale content (will be included in full issues)
return this.data.allItems?.length > 0
}
async fetch(issue) {
// Fetch some content
const data = await this.http.get(`/items`)
const allItems = data.items
// Optional: Helper functions to filter seen items, and mark unseen as seen in persisted
// storage once the issue has been printed. Provide an array and a picker function to
// get the unique ID.
const freshItems = this.filterSeenItems(allItems, item => item.id)
this.markSeenOnCleanUp(freshItems, item => item.id)
this.data = {
allItems,
freshItems,
}
}
render(issue) {
// Print different blocks depending on the issue type
// If it's an update print all fresh items
if (issue.updateOnly) {
return this.data.freshItems.map(item => new Blocks.PosterText(item.text))
}
// Otherwise print latest item
const [item] = this.data.allItems
return [
new Blocks.PosterText(item.text)
]
}
}
When calling super()
in your plugin class, pass a baseUrl
(and optionally headers
) to also
create a HTTP client:
super({
baseUrl: `https://www.instagram.com`,
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Cache-Control': 'max-age=0'
}
})
This will expose the client as this.http
, which you can use to make API requests easily.
async fetch({ updateOnly }) {
const data = await this.http.get(`/${process.env.INSTAGAM_USER}/?__a=1`)
// See Instagram plugin for examples...
})
The Block
base class exposes a few utility functions for drawing grayscale/dithered images,
positioning text and loading remoate/local images. Check core/Block.js
for details.
If you've created a cool plugin and would like it included in this repository please open a pull request with usage instructions.