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:
bin/hanami assets compile
bundle exec puma --config ./config/puma.rb
💡 Install Overmind and run overmind start
to run with full access to all processes (including remote debugging).
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
. -
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. This checks for updates every 12 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. This checks for updates every 5 minutes.
Both of these processes can be configured to use different polling intervals by passing in a different value in seconds (i.e. <poller>.new seconds: 60
). You can do this by modifying each script.
Each process runs in the background and are automatically configured in both the Procfile
and Procfile.dev
files. The latter is built for you when running bin/setup
.
The following APIs are supported. Each 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).
Used for displaying new content to your device. Your device’s refresh determines how often this occurs.
Request
Without Base64 Encryption
curl "https://localhost:2443/api/display/" \
-H 'ID: <redacted>' \
-H 'Access-Token: <redacted>' \
-H 'Accept: application/json' \
-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 'Accept: application/json' \
-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 'Accept: application/json' \
-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:
-
HTTP_BATTERY_VOLTAGE
: Must a a float (usually 0.0 to 4.1). -
HTTP_FW_VERSION
: The firmware version (i.e.1.2.3
). -
HTTP_HOST
: The host (usually the IP address). -
HTTP_REFRESH_RATE
: The refresh rate as saved on the device. Example: 100. -
HTTP_RSSI
: The signal strength (usually -100 to 100). -
HTTP_USER_AGENT
: The device name. -
HTTP_WIDTH
: The device width. Example: 800. -
HTTP_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 'Accept: application/json' \
-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.
Request
curl -X "POST" "https://localhost:2443/api/screens" \
-H 'Access-Token: <redacted>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d $'{
"image": {
"content": "<p>Test</p>"
"file_name": "test"
}
}'
The Access-Token
is your device’s MAC address. You can obtain this information from the UI.
Response
{
"path": "$HOME/Engineering/terminus/public/assets/screens/A1B2C3D4E5F6/test.bmp"
}
Uses 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 'Accept: application/json' \
-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.4.7",
"free_heap_size": 160656,
"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.
💡 The images API supports full HTML 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;
}
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
.
💡 The ID
is your device’s MAC and the Access-Token
is your device API Key.
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 do the following:
# 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
# Generate device screen with random name.
creator = Terminus::Screens::Creator.new
creator.call "<p>Test</p>",
Pathname(Hanami.app[:settings].screens_root).join("%<name>s.bmp")
#<Pathname:terminus/public/assets/screens/f5af3f06-775f-4ae9-8bb1-246d9a5200c9.bmp>
# To generate image with specific name.
creator.call "<p>Test.</p>", Pathname(Hanami.app[:settings].screens_root).join("demo.bmp")
#<Pathname:terminus/public/assets/screens/demo.bmp>
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:
Image Generation
creator = Terminus::Screens::Creator.new
creator.call(<<~CONTENT, Pathname(Hanami.app[:settings].screens_root).join("screens/%<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. 🎉
To build a Docker image, run:
bin/docker/build
To work within your Docker image, run:
bin/docker/console
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:
-
Colors: Use to customize site colors.
-
Defaults: Use to customize HTML element defaults.
-
Settings: Use to customize site settings.
-
Layout: Use to customize the site layout.
-
Components: Use to customize general site components.
-
View Transitions: Use to customize view transitions.
-
Dashboard: Use to customize the dashboard page.
-
Devices: Use to customize the devices page.
For responsive resolutions, the following measurements are used:
-
Extra Small: 300px
-
Small: 500px
-
Medium: 825px
-
Large: 1000px
-
Extra Large: 1500px
Docker is supported both for production and development purposes. Each is explained below.
To develop with Docker, there is a .devcontainer
folder that should provide the initial foundation from which to customize further for your needs.
To build an image for production purpose, 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.
-
Built with Hanamismith.
-
Engineered by TRMNL.