-
Allows you to run your own server.
-
Provides automatic detection of your server’s IP address.
-
Uses htmx.
-
Uses ImageMagick.
-
Uses Overmind.
-
Uses PostgreSQL.
-
Uses Puma.
-
Supports YJIT.
-
Supports Docker.
-
Supports TRMNL devices.
The following is a high level overview you can use to compare/contrast when deciding between using this Build Your Own Server (BYOS) implementation or our hosted solution.
Legend
-
⚪️ Planned.
-
🟢 Supported.
-
🟡 Partially supported.
-
🔴 Not supported, not implemented, or isn’t applicable.
Matrix
Terminus | Hosted | |
---|---|---|
Dashboard |
🟢 |
🟢 |
Auto-Provisioning |
🟢 |
🟢 |
Devices |
🟢 |
🟢 |
JSON Data API |
🟢 |
🟢 |
Image Previews |
🟢 |
🟢 |
Playlists |
🟡 |
🟢 |
Plugins |
🟢 |
🟢 |
Recipes |
🟢 |
🟢 |
Account |
🔴 |
🟢 |
Open Source Components |
🟢 |
🟡 |
Docker |
🟢 |
🔴 |
The goal isn’t for BYOS to match parity with our hosted solution but to provide enough of a pleasant solution for your own customized experience. There are trade offs either way but we’ve got you covered for whatever path you wish to travel. 🎉
To set up project, run:
git clone https://github.com/usetrmnl/byos_hanami terminus
cd terminus
bin/setup
To launch the server, run:
# Development
overmind start --port-step 10 --procfile Procfile.dev --can-die assets,migrate
# Production
overmind start --port-step 10 --can-die assets,migrate
To view the app, use either of the following:
-
Secure: https://localhost:2443
-
Insecure: http://localhost:2300
There are a few environment variables you can use to customize behavior:
-
API_URI
: Needed for connecting your device to this server. Defaults to your wired IP address. -
DATABASE_URL
: Necessary to connect to your PostgreSQL database. Can be customized by changing the value in the.env.development
or.env.test
file created when you ranbin/setup
. -
FIRMWARE_ROOT
: The root location for firmware updates. Defaults topublic/assets/firmware
. -
PREVIEWS_ROOT
: The root location for all device screen preview images when designing new screens. Defaults topublic/assets/previews
-
SCREENS_ROOT
: The root location for all device screens (images). Defaults topublic/assets/screens
.
There are a couple of ways you can provision a device with this server.
The first is automatic which happens immediately after you have successfully used the WiFi captive portal on your mobile phone to connect your TRMNL device to your local network where this server is running. You can also delete your device, via the UI, and it’ll be reconfigured for you automatically when the device next makes a Display API request.
The second way is to manually add your device via the UI. At a minimum, you only need to know your device’s MAC Address when entering your device information within the UI.
That’s it!
There are two background pollers that cache data from the remote Core server for improved performance:
-
Firmware (
bin/pollers/firmware
): Downloads the latest firmware for updating your local devices. By default, this checks for updates every six hours. -
Screen (
bin/pollers/screen
): Downloads device screens for any device you have set up to proxy to the Core server. You only need to toggle proxy support for any/all devices you want to pull from Core. This allows you to leverage any/all recipes/plugins you have configured via your remote account. By default, this checks for updates every five minutes.
All pollers can be configured to use different polling intervals by supplying the desired seconds in which to poll. You can do this by modifying each script. Example:
# bin/pollers/firmware
poller.call seconds: 60
# bin/pollers/screen
poller.call seconds: 30
Each automatically run in the background as separate processes in both the Procfile
and Procfile.dev
files. The latter is built for you when running bin/setup
.
When using Overmind, you can restart these pollers (as with any process managed by Overmind) as follows:
overmind restart firmware_poller
overmind restart screen_poller
This can be handy if you want to force either of these poller’s to check for new content.
To disable any of the pollers, remove them from the Procfile.dev
and/or Procfile
files. For example, delete these lines:
firmware_poller: bin/pollers/firmware screen_poller: bin/pollers/screen
You could also configure them to have a massive number of seconds as mentioned above when supplying custom seconds in which to poll.
By default, the Firmware Poller will automatically download the latest firmware but you’ll need to enable firmware updates for your device to have each new firmware release automatically applied. You can do this by editing your device and clicking the Firmware Update checkbox to enable. Otherwise, newer firmware versions will be cached on the server but your device won’t update.
In situations where your device updated to a newer Firmware version and it was a bad/broken version, you can revert to and older version by following these steps:
-
Ensure the device you want to downgrade has firmware updates turned on (you’ll also want to ensure devices you don’t want to downgrade have this setting turned off).
-
Visit the Firmwares page within the UI.
-
Delete all latest versions until you only have the version you want to downgrade to listed at the top of the list.
-
Short click the button on the back of your device to force a refresh and wait for the firmware downgrade to complete.
-
That’s it!
This section documents all supported API endpoints. Each endpoint uses HTTPS which requires accepting your locally generated SSL certificate. If you don’t want this behavior, you can switch to using HTTP (see above).
Some endpoints use either the HTTP ID
, Access-Token
or both headers. These break down as follows:
-
ID
: Your device’s MAC address. -
Access-Token
: Your device’s API key.
See each endpoint for further details.
Used for displaying new content to your device. Your device’s refresh rate determines how often this occurs.
Request
Without Base64 Encryption
curl "https://localhost:2443/api/display" \
-H 'ID: <redacted>' \
-H 'Access-Token: <redacted>' \
-H 'Content-Type: application/json'
With Base64 Encryption via HTTP Header
curl "https://localhost:2443/api/display" \
-H 'ID: <redacted>' \
-H 'Access-Token: <redacted>' \
-H 'Content-Type: application/json' \
-H 'BASE64: true'
With Base64 Encryption via Parameter
curl "https://localhost:2443/api/display?base_64=true" \
-H 'ID: <redacted>' \
-H 'Access-Token: <redacted>' \
-H 'Content-Type: application/json'
Both the ID
and Access-Token
HTTP headers are required for all of these API calls but these optional headers can be supplied as well which mimics what each device includes each request:
-
BATTERY_VOLTAGE
: Must a a float (usually 0.0 to 4.1). -
FW_VERSION
: The firmware version (i.e.1.2.3
). -
HOST
: The host (usually the IP address). -
REFRESH_RATE
: The refresh rate as saved on the device. Example: 100. -
RSSI
: The signal strength (usually -100 to 100). -
USER_AGENT
: The device name. -
WIDTH
: The device width. Example: 800. -
HEIGHT
: :The device height. Example: 480.
Response
Without Base64 Encryption
{
"filename": "demo.bmp",
"firmware_url": "http://localhost:2443/assets/firmware/1.4.8.bin",
"image_url": "https://localhost:2443/assets/screens/A1B2C3D4E5F6/demo.bmp",
"image_url_timeout": 0,
"refresh_rate": 130,
"reset_firmware": false,
"special_function": "sleep",
"update_firmware": false
}
With Base64 Encryption
{
"filename": "demo.bmp",
"firmware_url": "http://localhost:2443/assets/firmware/1.4.8.bin",
"image_url": "data:image/bmp;base64,<truncated>",
"image_url_timeout": 0,
"refresh_rate": 200,
"reset_firmware": false,
"special_function": "sleep",
"update_firmware": false
}
Uses for new device setup and then never used after.
Request
curl "https://localhost:2443/api/setup/" \
-H 'ID: <redacted>' \
-H 'Content-Type: application/json'
Response
{
"api_key": "<redacted>",
"friendly_id": "ABC123",
"image_url": "https://localhost:2443/assets/setup.bmp",
"message": "Welcome to TRMNL BYOS"
}
Used for generating new device screens by supplying HTML content for rendering, screenshotting, and grey scaling to render properly on your device. Both .png
or .bmp
extensions are supported for the file_name
key. If you don’t supply a file_name
, the server will generate one for you using a UUID for the file name. You can find all generated images in public/assets/screens
.
When making requests, the Access-Token
is your device’s MAC address. You can obtain this information from the UI.
Request (HTML Content)
curl -X "POST" "https://localhost:2443/api/screens" \
-H 'Access-Token: <redacted>' \
-H 'Content-Type: application/json' \
-d $'{
"image": {
"content": "<p>Test</p>",
"file_name": "demo.png"
}
}'
Full HTML is supported so you can supply CSS styles, full DOM, etc. At a minimum, you’ll want to use the following to prevent white borders showing up around your generated screens:
* {
margin: 0;
}
Don’t forget that you can use the Designer within the UI to build custom screens in real-time for faster feedback. The result of your work can be supplied to this endpoint to create a new screen for display on your device.
Request (URI)
curl -X "POST" "https://localhost:2443/api/screens" \
-H 'Access-Token: <redacted>' \
-H 'Content-Type: application/json' \
-d $'{
"image": {
"uri": "https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png",
"file_name": "demo.png",
"dimensions": "800x480!"
}
}'
The dimensions
parameter is optional and defaults to 800x480
when not supplied. You can use the full ImageMagick Geometry syntax as the value.
Request (Base64 Encoded Data)
curl -X "POST" "https://localhost:2443/api/screens" \
-H 'Access-Token: <redacted>' \
-H 'Content-Type: application/json' \
-d $'{
"image": {
"data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAAXNSR0IArs4c6QAAAAtJREFUCFtjYGAAAAADAAHc7H1IAAAAAElFTkSuQmCC"
"file_name": "demo.png",
"dimensions": "800x480!"
}
}'
The dimensions
parameter is optional and defaults to 800x480
when not supplied. You can use the full ImageMagick Geometry syntax as the value.
Response
{
"path": "$HOME/Engineering/terminus/public/assets/screens/A1B2C3D4E5F6/demo.png"
}
No matter what parameters you use for this request, you’ll always get a path (unless an error is encountered).
Used for logging information about your server and/or device. Mostly used for debugging purposes.
Request
## Logs
curl -X "POST" "https://localhost:2443/api/log" \
-H 'ID: <redacted>' \
-H 'Access-Token: <redacted>' \
-H 'Content-Type: application/json' \
-d $'{
"log": {
"logs_array": [
{
"log_id": 1,
"creation_timestamp": 1742022123,
"log_message": "returned code is not OK: 404",
"log_codeline": 597,
"device_status_stamp": {
"wifi_status": "connected",
"wakeup_reason": "timer",
"current_fw_version": "1.5.2",
"free_heap_size": 160656,
"max_alloc_size": 200000,
"special_function": "none",
"refresh_rate": 30,
"battery_voltage": 4.772,
"time_since_last_sleep_start": 31,
"wifi_rssi_level": -54
},
"additional_info": {
"retry_attempt": 1
},
"log_sourcefile": "src/bl.cpp"
}
]
}
}'
Response
Logs details and answers a HTTP 204 status with no content.
To contribute, run:
git clone https://github.com/usetrmnl/terminus
cd terminus
bin/setup
To access the console with direct access to all objects, run:
bin/console
Once in the console, you can interact with all objects. A few examples:
# Use a repository.
repository = Hanami.app["repositories.device"]
repository.all # View all devices.
device = repository.find 1 # Find by Device ID.
# Fetch next device screen. Sorts in descending order by modified timestamp.
fetcher = Hanami.app["aspects.screens.fetcher"]
fetcher.call device.slug
Should you want to create screens from the command line/console instead of using the Designer (UI) or Screens API, you can use the Creator
. Please be aware of the following:
-
The output path must include your device’s MAC Address in collapsed form (i.e. colons removed). Example:
"A1B2C3D4E5F6"
. -
Use
.bmp
or.png
as the file extension. This ensures your image is generated in the correct MIME Type and desired format. -
Use
%<name>s
in your output path to generate a random name. This must included a supported file extension. Example:%<name>s.png
. -
The
image
parameters used in the Screens API — exceptfile_name
— are identical to the keyword arguments when calling this object. -
When using
uri
anddata
keys, you’ll need to supply dimensions which leverage the full ImageMagick Geometry syntax.
Here are a few examples:
HTML content with random name
creator = Terminus::Screens::Creator.new
output_path = Pathname(Hanami.app[:settings].screens_root).join("A1B2C3D4E5F6/%<name>s.png")
creator.call output_path, content: "<p>Test</p>"
# Success(
# #<Pathname:terminus/public/assets/screens/A1B2C3D4E5F6/31a55181-ef94-4397-89d9-bd576d89b404.png>
# )
HTML content with specific name
creator = Terminus::Screens::Creator.new
output_path = Pathname(Hanami.app[:settings].screens_root).join("A1B2C3D4E5F6/demo.png")
creator.call output_path, content: "<p>Test</p>"
# Success(
# #<Pathname:terminus/public/assets/screens/A1B2C3D4E5F6/demo.png>
# )
URI
creator = Terminus::Screens::Creator.new
output_path = Pathname(Hanami.app[:settings].screens_root).join("A1B2C3D4E5F6/demo.png")
creator.call output_path,
uri: "https://leonardo.ai/wp-content/uploads/2023/07/image-129.jpeg",
dimensions: "800x480"
# Success(
# #<Pathname:terminus/public/assets/screens/A1B2C3D4E5F6/demo.png>
# )
Data (Base64)
creator = Terminus::Screens::Creator.new
output_path = Pathname(Hanami.app[:settings].screens_root).join("A1B2C3D4E5F6/demo.png")
creator.call output_path,
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAAXNSR0IArs4c6QAAAAtJREFUCFtjYGAAAAADAAHc7H1IAAAAAElFTkSuQmCC",
dimensions: "800x480!"
# Success(
# #<Pathname:terminus/public/assets/screens/A1B2C3D4E5F6/demo.png>
# )
When creating images, you might find this HTML template valuable as a starting point as this let’s you use the full capabilities of HTML to create new images for your device.
HTML Template
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<title>Demo</title>
<meta charset="utf-8">
<style type="text/css">
* {
margin: 0;
}
</style>
<script type="text/javascript">
</script>
</head>
<body>
<img src="uri/to/image" alt="Image"/>
</body>
</html>
Use of margin
zero is important to prevent default browser styles from creating borders around your image which will show up when rendered on your device. Otherwise, you have full capabilities to render any kind of page you want using whatever HTML you like. Anything is possible because Terminus::Screens::Creator
is designed to screenshot your rendered HTML as a 800x480 image to render on your device. If you put all this together, that means you can do this in the console:
Screen Creation
creator = Terminus::Screens::Creator.new
creator.call(<<~CONTENT, Pathname(Hanami.app[:settings].screens_root).join("A1B2C3D4E5F6/%<name>s.bmp"))
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<title>Demo</title>
<meta charset="utf-8">
<style type="text/css">
* {
margin: 0;
}
</style>
<script type="text/javascript">
</script>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
CONTENT
The above will create a new image in the public/screens
folder of this application which will eventually render on your device. 🎉
YJIT is enabled by default if detected which means you have built and installed Ruby with YJIT enabled. If you didn’t build Ruby with YJIT support, YJIT support will be ignored. That said, we recommend you enable YJIT support since the performance improvements are worth it.
💡 To enable YJIT globally, ensure the --yjit
flag is added to your RUBYOPT
environment variable. Example: export RUBYOPT="--yjit"
.
Pure CSS is used in order to avoid pulling in complicated frameworks. The following stylesheets allow you to customize the look and feel of this application as follows:
-
Settings: Use to customize site settings.
-
Colors: Use to customize site colors.
-
Keyframes: Use to customize keyframe behavior.
-
View Transitions: Use to customize view transitions.
-
Defaults: Use to customize HTML element defaults.
-
Layout: Use to customize the site layout.
-
Components: Use to customize general site components.
-
Dashboard: Use to customize the dashboard page.
-
Devices: Use to customize the devices page.
-
Designer: Use to customize the designer page.
For responsive resolutions, the following measurements are used:
-
Extra Small: 300px
-
Small: 500px
-
Medium: 825px
-
Large: 1000px
-
Extra Large: 1500px
The Santize gem is used to sanitize HTML/CSS when using the console, API, or UI. All of this configured via the Terminus::Sanitizer
class which defaults to the Sanitize::Config::RELAXED
style with additional support for style
and source
elements. If you find elements being stripped from your HTML/CSS content, this is why. Feel free to open an issue if you need additional support.
By default, all logging is set to INFO
level but you can get more verbose information by using the DEBUG
level. There are multiple ways to do this. First, you can export the desired debug level:
export LOG_LEVEL=debug
You can also specify the log level before launching the server:
LOG_LEVEL=debug overmind start --port-step 10 --procfile Procfile.dev --can-die assets,migrate
Finally, you can configure the app to use a different log level via lib/terminus/lib_container.rb
by adjusting log level of logger during registration:
register(:logger) { Cogger.new id: :terminus, level: :debug, formatter: :detail }
💡 See the Cogger gem documentation for further details.
Docker is supported both for production and development purposes. In most cases, you’ll want to use Docker Compose to manage the stack. Read on to learn more.
We use Docker Compose to quickly spin up the images and containers for running this application in either a development or production environment.
You’ll want to customize your API_URI
environment variable so the URI points to the server from where you are running the full stack. This is important because the API IP address shown via the Dashboard page will only show the URI of your Docker image/container if you don’t change this. You can do this by adding API_URI
to the environment section. Example: API_URI: http://192.168.1.1:2300
. You can also confirm this is set when launching all services and viewing the Dashboard (look for the API IP address).
The following commands might be of interest for getting started:
-
docker-compose up
: Builds and launches the entire stack. -
docker-compose build web
: Rebuilds the web service. You’ll want to run this before runningup
in order to pick up the latest changes whenever there is a new version release or pulling changes from themain
branch. -
docker-compose exec web bash
: This’ll give you a Bash shell within root of the project. Usebin/console
to launch a Hanami console. -
docker logs terminus-web-1
: Use this to view the web service logs.
Further details can be found in the compose.yml
file at the root of this project. The compose.yml
can also be configured via the following environment variables:
-
API_URI
: The host you are running your container on. Defaults to localhost but you’ll definitely want to replace localhost with your actual IP address because your device won’t be able to talk to your server otherwise. -
PG_USER
: Your PostgreSQL user name. -
PG_DATABASE
: Your PostgreSQL database name. -
PG_PASSWORD
: Your PostgreSQL password.
💡 The above is automatically generated for you when running bin/setup
but customization is encouraged.
To develop with Docker, you can use the same tooling as explained above in the Compose section and/or Production sections.
To build and deploy for production purposes, see the Compose section mentioned above. If you only care about the web image, then you can use the Dockerfile
and bin/docker
scripts. Here’s how each works:
-
bin/docker/build
: This will build a production Docker image based on latest changes to this project. -
bin/docker/console
: This will immediately give you a console for which to explore you Docker image from the command line. -
bin/docker/entrypoint
: This is used by theDockerfile
when building your Docker image.
The following is additional tooling, developed by the Open Source community, that might be of interest for use with this application:
-
Terminus Publisher: Provides a way to generate and publish content to Terminus for display on your device.
-
Built with Hanamismith.
-
Engineered by TRMNL.