Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
cc1dcbc
created turnOff outside the route
tiffany-tran07 Jul 4, 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
56505c9
moved turnOff outside the route
tiffany-tran07 Jul 14, 2025
6625e58
moved set-time route logic to update-sign
tiffany-tran07 Jul 14, 2025
c9096aa
Co-authored-by: charred-70 <charred-70@users.noreply.github.com>
tiffany-tran07 Jul 15, 2025
bb1fc19
Merge pull request #5 from Tyffoni/moveTurnOFf
evanugarte Jul 15, 2025
9bbccf9
add-sign-status-page (#6)
tiffany-tran07 Jul 15, 2025
802a22f
Merge branch 'master' into setTime
tiffany-tran07 Jul 17, 2025
548f63c
Merge pull request #4 from Tyffoni/setTime
tiffany-tran07 Jul 17, 2025
6e00adb
Update index.html
tiffany-tran07 Jul 17, 2025
2e8c16a
deleting print statements used for debugging
charred-70 Jul 19, 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
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
__pycache__/
node_modules/
.eslintcache
build/
config.json
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.8.4-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"]

15 changes: 15 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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

COPY *.py ./

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"
}
10 changes: 10 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: '2'
services:
sce_led_sign:
build:
context: .
dockerfile: ./Dockerfile.dev
ports:
- '10000:7000'
volumes:
- ./static:/app/templates
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.
201 changes: 201 additions & 0 deletions server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
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

threadExists = False
cancel_event = threading.Event()

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

def expire(exp):
if cancel_event.wait(timeout=exp):
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()
})



@app.route("/api/update-sign", methods=["POST", "GET"])
def update_sign():
global proc
global sign_message
global threadExists
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"
})

format_code = "%Y-%m-%dT%H:%M"
endTime = datetime.strptime(request.args.get("endTime"), format_code)
endTime = endTime.replace(tzinfo=ZoneInfo("America/Los_Angeles"))
currDate = datetime.now(ZoneInfo("America/Los_Angeles"))

ts = abs((endTime - currDate).total_seconds())

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

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

currThread.start()
threadExists = 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