Skip to content

Commit

Permalink
QR codes (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra committed Jan 28, 2024
1 parent 1836c20 commit 5c7468a
Show file tree
Hide file tree
Showing 18 changed files with 443 additions and 68 deletions.
13 changes: 13 additions & 0 deletions backend/app/api/frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ def api_frame_restart_event(id: int):
except Exception as e:
return jsonify({'error': 'Internal Server Error', 'message': str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR

@api.route('/frames/<int:id>/stop', methods=['POST'])
@login_required
def api_frame_stop_event(id: int):
try:
from app.tasks import stop_frame
stop_frame(id)
return 'Success', 200
except Exception as e:
return jsonify({'error': 'Internal Server Error', 'message': str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR

@api.route('/frames/<int:id>/deploy', methods=['POST'])
@login_required
def api_frame_deploy_event(id: int):
Expand Down Expand Up @@ -150,6 +160,9 @@ def api_frame_update(id: int):
if payload.get('next_action') == 'restart':
from app.tasks import restart_frame
restart_frame(frame.id)
if payload.get('next_action') == 'stop':
from app.tasks import stop_frame
stop_frame(frame.id)
elif payload.get('next_action') == 'deploy':
from app.tasks import deploy_frame
deploy_frame(frame.id)
Expand Down
7 changes: 4 additions & 3 deletions backend/app/models/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Frame(db.Model):
color = db.Column(db.String(256), nullable=True)
interval = db.Column(db.Double, default=300)
metrics_interval = db.Column(db.Double, default=60)
scaling_mode = db.Column(db.String(64), nullable=True) # cover (default), contain, stretch, center
scaling_mode = db.Column(db.String(64), nullable=True) # contain (default), cover, stretch, center
background_color = db.Column(db.String(64), nullable=True)
rotate = db.Column(db.Integer, nullable=True)
debug = db.Column(db.Boolean, nullable=True)
Expand Down Expand Up @@ -111,7 +111,7 @@ def new_frame(name: str, frame_host: str, server_host: str, device: Optional[str
status="uninitialized",
apps=[],
scenes=[create_default_scene()],
scaling_mode="cover",
scaling_mode="contain",
rotate=0,
background_color="white",
device=device or "web_only",
Expand Down Expand Up @@ -192,6 +192,7 @@ def create_default_scene() -> dict:

def get_frame_json(frame: Frame) -> dict:
frame_json = {
"frameHost": frame.frame_host,
"framePort": frame.frame_port or 8787,
"serverHost": frame.server_host or "localhost",
"serverPort": frame.server_port or 8989,
Expand All @@ -204,7 +205,7 @@ def get_frame_json(frame: Frame) -> dict:
"interval": frame.interval or 30.0,
"metricsInterval": frame.metrics_interval or 60.0,
"debug": frame.debug or False,
"scalingMode": frame.scaling_mode or "cover",
"scalingMode": frame.scaling_mode or "contain",
"rotate": frame.rotate or 0,
}

Expand Down
1 change: 1 addition & 0 deletions backend/app/tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .deploy_frame import deploy_frame # noqa: F401 (breaks huey)
from .reset_frame import reset_frame # noqa: F401 (breaks huey)
from .restart_frame import restart_frame # noqa: F401 (breaks huey)
from .stop_frame import stop_frame # noqa: F401 (breaks huey)

@huey.signal(SIGNAL_LOCKED)
def task_not_run_handler(signal, task, exc=None):
Expand Down
35 changes: 35 additions & 0 deletions backend/app/tasks/stop_frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@


from app import create_app
from app.huey import huey
from app.models.log import new_log as log
from app.models.frame import Frame, update_frame
from app.utils.ssh_utils import get_ssh_connection, exec_command, remove_ssh_connection

@huey.task()
def stop_frame(id: int):
app = create_app()
with app.app_context():
ssh = None
try:
frame = Frame.query.get_or_404(id)

frame.status = 'stopping'
update_frame(frame)

ssh = get_ssh_connection(frame)
exec_command(frame, ssh, "sudo systemctl stop frameos.service || true")
exec_command(frame, ssh, "sudo systemctl disable frameos.service")

frame.status = 'stopped'
update_frame(frame)

except Exception as e:
log(id, "stderr", str(e))
frame.status = 'uninitialized'
update_frame(frame)
finally:
if ssh is not None:
ssh.close()
log(id, "stdinfo", "SSH connection closed")
remove_ssh_connection(ssh)
1 change: 1 addition & 0 deletions frameos/frame.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"framePort": 8787,
"frameHost": "localhost",
"serverHost": "mariuss-macbook-air",
"serverPort": 8989,
"serverApiKey": "test-api-key",
Expand Down
1 change: 1 addition & 0 deletions frameos/frameos.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ requires "jester >= 0.6.0"
requires "linuxfb >= 0.1.0"
requires "psutil >= 0.6.0"
requires "ws >= 0.5.0"
requires "qrgen >= 3.0.0"

taskRequires "assets", "nimassets >= 0.2.4"

Expand Down
10 changes: 10 additions & 0 deletions frameos/nimble.lock
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@
"checksums": {
"sha1": "ae4daf4ae302d0431f3c2d385ae9d2fe767a3246"
}
},
"QRgen": {
"version": "3.0.0",
"vcsRevision": "aa10edbf292513c1c636ab8afe960a037a55be4b",
"url": "https://github.com/aruZeta/QRgen",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "df0c1d5ec4188c508b51b065b196aa63e0c6e924"
}
}
},
"tasks": {
Expand Down
201 changes: 201 additions & 0 deletions frameos/src/apps/qr/app.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import json, strformat
import pixie
import frameos/types
import QRgen
import
QRgen/private/[
DrawedQRCode/DrawedQRCode,
DrawedQRCode/print,
Drawing,
qrTypes
]

type
AppConfig* = object
code*: string
size*: float
sizeUnit*: string
alRad*: float
moRad*: float
moSep*: float
position*: string
offsetX*: float
offsetY*: float
padding*: int
qrCodeColor*: Color
backgroundColor*: Color

App* = ref object
nodeId*: NodeId
scene*: FrameScene
frameConfig*: FrameConfig
appConfig*: AppConfig

proc init*(nodeId: NodeId, scene: FrameScene, appConfig: AppConfig): App =
result = App(
nodeId: nodeId,
scene: scene,
frameConfig: scene.frameConfig,
appConfig: appConfig,
)

proc log*(self: App, message: string) =
self.scene.logger.log(%*{"event": &"{self.nodeId}:log", "message": message})

proc error*(self: App, message: string) =
self.scene.logger.log(%*{"event": &"{self.nodeId}:error", "error": message})

# The "renderImg" function is copied from:
# https://raw.githubusercontent.com/aruZeta/QRgen/main/src/QRgen/renderer.nim
#
# I adapted it to support variable width padding.
# TODO: patch and merge upstream

template size: uint8 =
## Helper template to get the size of the passed `DrawedQRCode`'s `drawing`.
self.drawing.size

func genDefaultCoords(self: DrawedQRCode): tuple[x, y, w, h: uint8] =
let size: uint8 = (self.drawing.size div (
case self.ecLevel
of qrECL: 24
of qrECM: 12
of qrECQ: 6
of qrECH: 3
) div 2) * 2 + 1
let margin: uint8 = (self.drawing.size - size) div 2
result = (
x: margin,
y: margin,
w: size,
h: size
)

proc renderImg*(
self: DrawedQRCode,
light: string = "#ffffff",
dark: string = "#000000",
alRad: Percentage = 0,
moRad: Percentage = 0,
moSep: Percentage = 0,
pixels: uint32 = 512,
padding: uint8 = 2,
img: Image = Image(width: 0, height: 0),
imgCoords: tuple[x, y, w, h: uint8] = self.genDefaultCoords
): Image =
let
modules: uint8 = self.drawing.size + padding * 2
modulePixels: uint16 = (pixels div modules).uint16
pixelsMargin: uint16 = (pixels mod modules).uint16 div 2 + modulePixels*(padding)
actualSize: uint32 = modulePixels.uint32*(modules-(padding * 2)) + (pixelsMargin+1)*2
let pixels: uint32 =
if actualSize < pixels: actualSize
else: pixels
result = newImage(pixels.int, pixels.int)
result.fill(light)
let ctx: Context = result.newContext
ctx.fillStyle = dark
ctx.strokeStyle = dark
template calcPos(modulePos: uint8): float32 =
(pixelsMargin + modulePos * modulePixels).float32
template drawRegion(ax, bx, ay, by: uint8, f: untyped) {.dirty.} =
for y in ay..<by:
for x in ax..<bx:
if self.drawing[x, y]:
let pos = vec2(x.calcPos + moSepPx, y.calcPos + moSepPx)
f
template drawQRModulesOnly(f: untyped) {.dirty.} =
drawRegion 0'u8, size, 7'u8, size-7, f
drawRegion 7'u8, size-7, 0'u8, 7'u8, f
drawRegion 7'u8, size, size-7, size, f
if moRad > 0 or moSep > 0:
let
moSepPx: float32 = modulePixels.float32 * 0.4 * moSep / 100
s: Vec2 = vec2(
modulePixels.float32 - moSepPx*2,
modulePixels.float32 - moSepPx*2
)
moRadPx: float32 = (modulePixels.float32 / 2 - moSepPx) * moRad / 100
drawQRModulesOnly ctx.fillRoundedRect(rect(pos, s), moRadPx)
else:
let
moSepPx: float32 = 0
s: Vec2 = vec2(
modulePixels.float32,
modulePixels.float32
)
if alRad > 0:
drawQRModulesOnly ctx.fillRect(rect(pos, s))
else:
drawRegion 0'u8, size, 0'u8, size, ctx.fillRect(rect(pos, s))
if alRad > 0 or moRad > 0 or moSep > 0:
let alRadPx: float32 = 3.5 * alRad / 100
template innerRadius(lvl: static range[0'i8..2'i8]): float32 =
when lvl == 0: alRadPx
else:
if alRadPx == 0: 0f
elif alRadPx-lvl <= 0: 1f / (lvl * 2)
else: alRadPx-lvl
template drawAlPatterns(lvl: range[0'i8..2'i8], c: untyped) {.dirty.} =
template s1: float32 = ((7-lvl*2) * modulePixels).float32
template s: Vec2 {.dirty.} = vec2(s1, s1)
template r: float32 = innerRadius(lvl) * modulePixels.float32
template vec2F(a, b: untyped): Vec2 = vec2(a.calcPos, b.calcPos)
when c == "light":
ctx.fillStyle = light
ctx.strokeStyle = light
ctx.fillRoundedRect rect(vec2F(0'u8+lvl, 0'u8+lvl), s), r
ctx.fillRoundedRect rect(vec2F(size-7'u8+lvl, 0'u8+lvl), s), r
ctx.fillRoundedRect rect(vec2F(0'u8+lvl, size-7+lvl), s), r
when c == "light":
ctx.fillStyle = dark
ctx.strokeStyle = dark
drawAlPatterns 0, "dark"
drawAlPatterns 1, "light"
drawAlPatterns 2, "dark"
if img.width > 0 and img.height > 0:
template calc(n: uint8): float32 = (n * modulePixels).float32
ctx.drawImage(
img,
(calc imgCoords.x) + pixelsMargin.float32,
(calc imgCoords.y) + pixelsMargin.float32,
calc imgCoords.w,
calc imgCoords.h
)

proc run*(self: App, context: ExecutionContext) =
let code = if self.appConfig.code == "": (if self.frameConfig.framePort mod 1000 == 443: "https" else: "http") &
"://" & self.frameConfig.frameHost & ":" & $self.frameConfig.framePort else: self.appConfig.code
let myQR = newQR(code)

let width = case self.appConfig.sizeUnit
of "percent": self.appConfig.size / 100.0 * min(context.image.width, context.image.height).float
of "pixels per dot": self.appConfig.size * (myQR.drawing.size.int + self.appConfig.padding * 2).float
else: self.appConfig.size

let qrImage = myQR.renderImg(
light = self.appConfig.backgroundColor.toHtmlHex,
dark = self.appConfig.qrCodeColor.toHtmlHex,
alRad = self.appConfig.alRad,
moRad = self.appConfig.moRad,
moSep = self.appConfig.moSep,
pixels = width.uint32,
padding = self.appConfig.padding.uint8
)

let xAlign = case self.appConfig.position:
of "top-left", "center-left", "bottom-left": self.appConfig.offsetX
of "top-right", "center-right", "bottom-right": context.image.width.float - qrImage.width.float +
self.appConfig.offsetX
else: (context.image.width.float - qrImage.width.float) / 2.0 + self.appConfig.offsetX

let yAlign = case self.appConfig.position:
of "top-left", "top-center", "top-right": self.appConfig.offsetY
of "bottom-left", "bottom-center", "bottom-right": context.image.height.float - qrImage.height.float +
self.appConfig.offsetY
else: (context.image.height.float - qrImage.height.float) / 2.0 + self.appConfig.offsetY

context.image.draw(
qrImage,
translate(vec2(xAlign, yAlign))
)
Loading

0 comments on commit 5c7468a

Please sign in to comment.