Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7c3531f
init
neelp03 Apr 4, 2022
016fa62
Create README.md
neelp03 Apr 4, 2022
7e33873
tunnel to corev4
Aug 16, 2022
aeecbc7
Added Monitoring to it
Sep 24, 2022
778dbe5
oops
Sep 24, 2022
07ca51c
remove text-scroller binary
Apr 6, 2023
214b041
reopen ssh tunnel if clark has not pinged sign in over 1 minute
evanugarte Apr 6, 2023
a7aba96
move time.sleep to top of loop
evanugarte Apr 6, 2023
a46c27c
ssh_tunnel_last_opened initial value is when tunnel was last opened
evanugarte Apr 13, 2023
be6442c
reopen tunnel if its been 2 minutes instead of 60 seconds
evanugarte Apr 13, 2023
c07b4d5
rename neel metric to last_health_check_request
evanugarte Sep 7, 2023
98c3fa7
remove cadvisor and prometheus containers
evanugarte Sep 7, 2023
3c3fc56
remove debug steps in Dockerfile
evanugarte Sep 7, 2023
86d7922
bring back sce sign exe copy
evanugarte Sep 7, 2023
4005181
add copying of font file to sce sign
evanugarte Sep 29, 2023
f0e3b1e
Add dev docker compose and index.html
evanugarte Jun 18, 2025
469f12c
if development then just print the command
evanugarte Jun 20, 2025
309df82
add change
tiffany-tran07 Jun 25, 2025
08d7783
fixed initial sign message
tiffany-tran07 Jun 25, 2025
229657e
Co-authored-by: charred-70 <charred-70@users.noreply.github.com>
tiffany-tran07 Jun 27, 2025
a609d73
render html test
tiffany-tran07 Jul 3, 2025
9cfb356
actually brought the turnOff outside the route
tiffany-tran07 Jul 4, 2025
7a6cc75
removed comments
tiffany-tran07 Jul 4, 2025
fce510d
Revert "transferred "tied shoes" logic to rpi controller"
tiffany-tran07 Jul 13, 2025
6625e58
moved set-time route logic to update-sign
tiffany-tran07 Jul 14, 2025
eb6f10b
created turnOff outside the route
tiffany-tran07 Jul 4, 2025
02dcbef
Update index.html
tiffany-tran07 Jul 17, 2025
0a1a1e6
fix turn off function, insert expiration
tiffany-tran07 Jul 20, 2025
f0e50a4
delete pycache
tiffany-tran07 Jul 20, 2025
fafbb1d
add pycache to gitignore
tiffany-tran07 Jul 20, 2025
3aa5793
resolved comments, changed min timezone
tiffany-tran07 Jul 21, 2025
0bcee6b
use local time in frontend; add timezone to backend
tiffany-tran07 Jul 22, 2025
0b2054e
fixed expiration
tiffany-tran07 Jul 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
__pycache__/
node_modules/
.eslintcache
build/
config.json
__pycache__
File renamed without changes.
25 changes: 25 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
FROM python:3.13-slim-buster


WORKDIR /app

RUN apt-get update && apt-get install jq ssh -y

COPY requirements.txt .
RUN python3 -m pip install -r requirements.txt
COPY tun.sh .
COPY ./config/config.json /app/config/
COPY sce_sign.exe .

COPY *.py ./

COPY 10x20.bdf .

RUN chmod +x sce_sign.exe
RUN chmod +x tun.sh
RUN mkdir /app/ssh

EXPOSE 5000

ENTRYPOINT ["/app/tun.sh"]

14 changes: 14 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM python:3.13


WORKDIR /app

RUN apt-get update && apt-get install jq ssh -y

COPY requirements.txt .
RUN python3 -m pip install -r requirements.txt

EXPOSE 7000

ENTRYPOINT ["python", "server.py", "--development", "--port=7000"]

24 changes: 1 addition & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,2 @@
# rpi-led-controller
Controlling an LED matrix from a website hosted on a raspberry pi

## Prerequisites:
- 2 [64x32 RGB LED Matrix](https://www.adafruit.com/product/2279)
- [Adafruit RGB Matrix Bonnet for Raspberry Pi](https://www.adafruit.com/product/3211)
- Any normal size raspberry pi (so that the bonnet can fit on top of it)

## Running the website
1. Boot your Pi with raspbian and with a network connection
2. Follow the wiring guide from @hzeller ([link](https://github.com/hzeller/rpi-rgb-led-matrix/blob/master/wiring.md))
3. Install docker and docker-compose
4. Run the website with:
```
sudo docker-compose up
```
**Note:** we use sudo to give docker access to the pi's GPIO pins.

5. After some time, the website should appear, accessible from port 80 on the pi:
![image](https://user-images.githubusercontent.com/36345325/140591851-08f9b5b5-c92d-4286-a5c8-60ceb5c45ba1.png)

The random button yields a random phrase and colors to test the sign:

![image](https://user-images.githubusercontent.com/36345325/140591859-227baa9f-444c-48dd-9f44-b7b50376346e.png)
Python code that controls the LED sign in the SCE room
3 changes: 3 additions & 0 deletions config/config.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"CORE_V4_IP" : "XXX.XXX.XXX.XXX"
}
13 changes: 13 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: '2'
services:
sce_led_sign:
build:
context: .
dockerfile: ./Dockerfile.dev
restart: always
ports:
- '10000:7000'
volumes:
- ./static:/app/templates
- ./server.py:/app/server.py
- ./sign_message.py:/app/sign_message.py
34 changes: 10 additions & 24 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,16 @@
version: '2'
services:
server:
container_name: server
sce_led_sign:
container_name: sce_led_sign
build:
context: ./server
dockerfile: ./Dockerfile
ports:
- '8080:8080'
restart: 'on-failure'
networks:
- local
privileged: true
website:
container_name: website
build:
context: ./website
context: .
dockerfile: ./Dockerfile
volumes:
- ~/.ssh/known_hosts:/app/ssh/known_hosts
- ~/.ssh/id_rsa:/app/ssh/id_rsa
- ~/.ssh/id_rsa.pub:/app/ssh/id_rsa.pub
- ./static:/app/templates
ports:
- '80:80'
restart: 'on-failure'
networks:
- local
networks:
local:
driver: bridge
ipam:
config:
- subnet: 172.16.0.0/24
gateway: 172.16.0.254
tty: true
privileged: true
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Flask==2.1.0
prometheus_client>=0.14.1
requests>=2.20.1
Werkzeug==2.2.2
Binary file added sce_sign.exe
Binary file not shown.
209 changes: 209 additions & 0 deletions server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import argparse
from flask import Flask, request, jsonify, render_template
from os import sep, path
from prometheus_client import Gauge, generate_latest
import random
import subprocess
import threading
import time
from datetime import datetime
from zoneinfo import ZoneInfo

from subprocess import Popen, PIPE, STDOUT

from sign_message import SignMessage

proc = None
sign_message = None

message_is_pending_expiration = False
cancel_event = threading.Event()

def turnOff():
global proc
global sign_message
success = False
if args.development:
sign_message = None
if proc != None:
proc.kill()
sign_message = None
success = True
return success

def expire(exp):
if cancel_event.wait(timeout=exp):
turnOff()
return
turnOff()

app = Flask(__name__)
parser = argparse.ArgumentParser()
parser.add_argument(
'--development',
action='store_true',
help='if set, runs in dev mode (skip ssh tunnel and running binary file)',
)
parser.add_argument(
'--port',
type=int,
default=80,
help='port for server to listen on',
)
args = parser.parse_args()

last_health_check_request = Gauge(
'last_health_check_request',
'the last time the server recieved an HTTP GET request to /api/health-check',
)
ssh_tunnel_last_opened = Gauge('ssh_tunnel_last_opened', 'the last time we opened the ssh tunnel')

def hex_to_rgb(hex_value):
return ",".join([str(int(hex_value[i:i+2], 16)) for i in (0, 2, 4)])

def maybe_reopen_ssh_tunnel():
"""
if we havent recieved a health check ping in over 1 min then
we rerun the script to open the ssh tunnel.
"""
while 1:
time.sleep(60)
now_epoch_seconds = int(time.time())
# skip reopening the tunnel if the value is 0 or falsy
if not last_health_check_request._value.get():
continue
if now_epoch_seconds - last_health_check_request._value.get() > 120:
ssh_tunnel_last_opened.set(now_epoch_seconds)
subprocess.Popen(
'./tun.sh tunnel-only',
shell=True,
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
)


@app.route("/api/health-check", methods=["GET"])
def health_check():
last_health_check_request.set(int(time.time()))
global sign_message
if sign_message:
return jsonify(sign_message.to_dict())
else:
return jsonify({
"success": True
})

@app.route("/metrics", methods=["GET"])
def metrics():
return generate_latest()

@app.route("/api/random", methods=["GET"])
def random_message():
text = subprocess.check_output(
"sort -R random.txt | head -n1",
shell=True).decode('utf-8').strip().replace("\\", "")
text_color = "#%06x" % random.randint(0, 0xFFFFFF)
background_color = "#%06x" % random.randint(0, 0xFFFFFF)
border_color = "#%06x" % random.randint(0, 0xFFFFFF)
return jsonify({
"scrollSpeed": 10,
"backgroundColor": background_color,
"textColor": text_color,
"borderColor": border_color,
"text": text,
"success": True
})


@app.route("/api/turn-off", methods=["GET"])
def turn_off():
return jsonify({
"success": turnOff()
})


# this should be a POST only
# change your fetch in the frontend in the click handler to make an "HTTP POST" request
@app.route("/api/update-sign", methods=["POST", "GET"])
def update_sign():
global proc
global sign_message
global message_is_pending_expiration
global cancel_event

if request.method == "GET":
if sign_message == None:
return jsonify({
"Sign": "Already Expired"
})

if request.args.get("endTime") == '':
return jsonify({
"Sign": "Never Expires"
})

# you likely need to update the below to correctly parse the format of
# toISOString() being used in the frontend

format_code = "%Y-%m-%dT%H:%M"
endTime = datetime.strptime(request.args.get("endTime"), format_code)
# ensure that both the date passed from the frontend and the "now" we are
# calculating below are both in UTC timezone, use print(..., flush = True)
currDate = datetime.now()
print(endTime, flush = True)
ts = abs((endTime - currDate).total_seconds())

if message_is_pending_expiration:
cancel_event.set()
cancel_event = threading.Event()

currThread = threading.Thread(target=expire, args=(ts,))

currThread.start()
message_is_pending_expiration = True
return jsonify({
"endTime": endTime,
"today": currDate,
"time": ts
})

data = request.json
CURRENT_DIRECTORY = path.dirname(path.abspath(__file__)) + sep

success = False
if proc != None:
proc.kill()
try:
if data and len(data):
sign_message = SignMessage(data)
command = sign_message.to_subprocess_command()
print("running command", command, flush=True)
if not args.development:
proc = subprocess.Popen(command)
success = True
return jsonify({
"success": success
})
except Exception as e:
print(e, flush=True)
sign_message = None
return "Could not update sign", 500



@app.route('/')
def home():
return render_template('index.html')

if __name__ == "__main__":
# give the last opened an initial value of now,
# since upon starting the led sign the tunnel should
# be open
ssh_tunnel_last_opened.set(int(time.time()))
if not args.development:
t = threading.Thread(
target=maybe_reopen_ssh_tunnel,
daemon=True,
)
t.start()
app.run(host="0.0.0.0", port=args.port, debug=True, threaded=True)
11 changes: 0 additions & 11 deletions server/Dockerfile

This file was deleted.

14 changes: 0 additions & 14 deletions server/README.md

This file was deleted.

Loading