Skip to content

Commit

Permalink
Event payloads in the editor (#59)
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra committed Jan 29, 2024
1 parent 5c7468a commit 1efe80c
Show file tree
Hide file tree
Showing 15 changed files with 246 additions and 231 deletions.
2 changes: 1 addition & 1 deletion backend/app/api/frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def api_frame_get_image(id: int):
def api_frame_render_event(id: int):
frame = Frame.query.get_or_404(id)
try:
response = requests.get(f'http://{frame.frame_host}:{frame.frame_port}/event/render')
response = requests.post(f'http://{frame.frame_host}:{frame.frame_port}/event/render')
if response.status_code == 200:
return "OK", 200
else:
Expand Down
5 changes: 2 additions & 3 deletions backend/app/api/tests/test_frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,13 @@ def test_api_frame_get_image_external_service_error(self):
assert response.status_code == 500
assert json.loads(response.data) == { "error": "Unable to fetch image" }


def test_api_frame_render_event_success(self):
with mock.patch('requests.get', return_value=MockResponse(status_code=200)):
with mock.patch('requests.post', return_value=MockResponse(status_code=200)):
response = self.client.post(f'/api/frames/{self.frame.id}/event/render')
assert response.status_code == 200

def test_api_frame_render_event_failure(self):
with mock.patch('requests.get', return_value=MockResponse(status_code=500)):
with mock.patch('requests.post', return_value=MockResponse(status_code=500)):
response = self.client.post(f'/api/frames/{self.frame.id}/event/render')
assert response.status_code == 500

Expand Down
5 changes: 5 additions & 0 deletions backend/app/codegen/scene_nim.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ def node_id_to_integer(node_id: str) -> int:
if not node_fields.get(source):
node_fields[source] = {}
node_fields[source][field] = target
if source_handle.startswith('code/') and target_handle.startswith('fieldInput/'):
field = target_handle.replace('fieldInput/', '')
if not field_inputs.get(target):
field_inputs[target] = {}
field_inputs[target][field] = source_handle.replace('code/', '')

for node in nodes:
node_id = node['id']
Expand Down
2 changes: 1 addition & 1 deletion frameos/frameos.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +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"
requires "qrgen >= 3.1.0"

taskRequires "assets", "nimassets >= 0.2.4"

Expand Down
6 changes: 3 additions & 3 deletions frameos/nimble.lock
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,13 @@
}
},
"QRgen": {
"version": "3.0.0",
"vcsRevision": "aa10edbf292513c1c636ab8afe960a037a55be4b",
"version": "3.1.0",
"vcsRevision": "4445bb93a126ec33004901e9486963a40cbb97fc",
"url": "https://github.com/aruZeta/QRgen",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "df0c1d5ec4188c508b51b065b196aa63e0c6e924"
"sha1": "41a7a63b515a4cad66091a70509925e2dcc5a0e4"
}
}
},
Expand Down
126 changes: 1 addition & 125 deletions frameos/src/apps/qr/app.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@ import json, strformat
import pixie
import frameos/types
import QRgen
import
QRgen/private/[
DrawedQRCode/DrawedQRCode,
DrawedQRCode/print,
Drawing,
qrTypes
]
import QRgen/renderer

type
AppConfig* = object
Expand Down Expand Up @@ -45,124 +39,6 @@ proc log*(self: App, message: string) =
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
Expand Down
18 changes: 7 additions & 11 deletions frameos/src/frameos/runner.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import json, pixie, times, options, asyncdispatch, strformat, locks
import json, pixie, times, options, asyncdispatch, strformat, strutils, locks
import pixie/fileformats/png
import scenes/default as defaultScene

Expand Down Expand Up @@ -164,28 +164,24 @@ proc startMessageLoop*(self: RunnerThread): Future[void] {.async.} =
let (success, (event, payload)) = eventChannel.tryRecv()
if success:
waitTime = 1
if not event.startsWith("mouse"):
self.logger.log(%*{"event": "event:" & event, "payload": payload})
try:
case event:
of "render":
self.logger.log(%*{"event": "event:" & event})
self.triggerRenderNext = true
continue
of "turnOn":
self.logger.log(%*{"event": "event:" & event})
drivers.turnOn()
of "turnOff":
self.logger.log(%*{"event": "event:" & event})
drivers.turnOff()
of "mouseMove":
if self.frameConfig.width > 0 and self.frameConfig.height > 0:
payload["x"] = %*((self.frameConfig.width.float * payload["x"].getInt().float / 32767.0).int)
payload["y"] = %*((self.frameConfig.height.float * payload["y"].getInt().float / 32767.0).int)
self.dispatchSceneEvent(event, payload)
of "mouseUp", "mouseDown", "keyUp", "keyDown", "button":
if event == "button":
self.logger.log(%*{"event": "event:" & event, "payload": payload})
self.dispatchSceneEvent(event, payload)
else:
self.logger.log(%*{"event": "event:" & event, "payload": payload})
else: discard

self.dispatchSceneEvent(event, payload)
except Exception as e:
self.logger.log(%*{"event": "event:error", "error": $e.msg,
"stacktrace": e.getStackTrace()})
Expand Down
108 changes: 51 additions & 57 deletions frameos/src/frameos/server.nim
Original file line number Diff line number Diff line change
Expand Up @@ -31,63 +31,57 @@ proc sendToAll(message: string) {.async.} =
if connection.readyState == Open:
asyncCheck connection.send(message)

proc match(request: Request): Future[ResponseData] {.async.} =
{.cast(gcsafe).}:
block route:
case request.pathInfo
of "/":
let scalingMode = case globalFrameConfig.scalingMode:
of "cover", "center":
globalFrameConfig.scalingMode
of "stretch":
"100% 100%"
else:
"contain"
resp Http200, indexHtml.replace("/*$scalingMode*/contain", scalingMode)
of "/ws":
var ws = await newWebSocket(request)
router myrouter:
get "/":
{.gcsafe.}: # We're only reading static assets. It's fine.
let scalingMode = case globalFrameConfig.scalingMode:
of "cover", "center":
globalFrameConfig.scalingMode
of "stretch":
"100% 100%"
else:
"contain"
resp Http200, indexHtml.replace("/*$scalingMode*/contain", scalingMode)
get "/ws":
{.gcsafe.}: # We're only modifying globals via locks. It's fine.
var ws = await newWebSocket(request)
try:
log(%*{"event": "websocket:connect", "key": ws.key})
withLock connectionsLock:
connections.add ws
while ws.readyState == Open:
let packet = await ws.receiveStrPacket()
log(%*{"event": "websocket:message", "message": packet})
# TODO: accept events?
except WebSocketError:
log(%*{"event": "websocket:disconnect", "key": ws.key, "reason": getCurrentExceptionMsg()})
withLock connectionsLock:
let index = connections.find(ws)
if index >= 0:
connections.delete(index)
post "/event/@name":
log(%*{"event": "http", "post": request.pathInfo})
let payload = parseJson(if request.body == "": "{}" else: request.body)
sendEvent(@"name", payload)
resp Http200, {"Content-Type": "application/json"}, $(%*{"status": "ok"})
get "/image":
log(%*{"event": "http", "get": request.pathInfo})
{.gcsafe.}: # We're reading immutable globals and png data via a lock. It's fine.
try:
let image = drivers.toPng(360 - globalFrameConfig.rotate)
if image != "":
resp Http200, {"Content-Type": "image/png"}, image
else:
raise newException(Exception, "No image available")
except Exception:
try:
log(%*{"event": "websocket:connect", "key": ws.key})
withLock connectionsLock:
connections.add ws
while ws.readyState == Open:
let packet = await ws.receiveStrPacket()
log(%*{"event": "websocket:message", "message": packet})
# TODO: accept (debounced) render requests?
except WebSocketError:
log(%*{"event": "websocket:disconnect", "key": ws.key, "reason": getCurrentExceptionMsg()})
withLock connectionsLock:
let index = connections.find(ws)
if index >= 0:
connections.delete(index)
of "/event/render":
log(%*{"event": "http", "path": request.pathInfo})
globalRunner.triggerRender()
resp Http200, {"Content-Type": "application/json"}, $(%*{"status": "ok"})
of "/event/turnOn":
log(%*{"event": "http", "path": request.pathInfo})
globalRunner.sendEvent("turnOn", %*{})
resp Http200, {"Content-Type": "application/json"}, $(%*{"status": "ok"})
of "/event/turnOff":
log(%*{"event": "http", "path": request.pathInfo})
globalRunner.sendEvent("turnOff", %*{})
resp Http200, {"Content-Type": "application/json"}, $(%*{"status": "ok"})
of "/image":
log(%*{"event": "http", "path": request.pathInfo})
try:
let image = drivers.toPng(360 - globalFrameConfig.rotate)
if image != "":
resp Http200, {"Content-Type": "image/png"}, image
else:
raise newException(Exception, "No image available")
except Exception:
try:
resp Http200, {"Content-Type": "image/png"}, getLastPng()
except Exception as e:
resp Http200, {"Content-Type": "image/png"}, renderError(globalFrameConfig.renderWidth(),
globalFrameConfig.renderHeight(), &"Error: {$e.msg}\n{$e.getStackTrace()}").encodeImage(PngFormat)
else:
resp Http404, "Not found!"
resp Http200, {"Content-Type": "image/png"}, getLastPng()
except Exception as e:
resp Http200, {"Content-Type": "image/png"}, renderError(globalFrameConfig.renderWidth(),
globalFrameConfig.renderHeight(), &"Error: {$e.msg}\n{$e.getStackTrace()}").encodeImage(PngFormat)
error Http404:
log(%*{"event": "404", "path": request.pathInfo})
resp Http404, "Not found!"

proc listenForRender*() {.async.} =
var hasConnections = false
Expand All @@ -109,7 +103,7 @@ proc newServer*(frameOS: FrameOS): Server =

let port = (frameOS.frameConfig.framePort or 8787).Port
let settings = newSettings(port = port)
var jester = initJester(matcher = match.MatchProc, settings = settings)
var jester = initJester(myrouter, settings)

result = Server(
frameConfig: frameOS.frameConfig,
Expand Down
5 changes: 2 additions & 3 deletions frameos/src/frameos/utils/font.nim
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import assets/fonts as fontAssets

var typeface: Option[Typeface] = none(TypeFace)

proc getDefaultTypeface*(): Typeface {.gcsafe.} =
# We assume nobody overrides this font in a thread. Worse case they should override to the same data.
{.cast(gcsafe).}:
proc getDefaultTypeface*(): Typeface =
{.cast(gcsafe).}: # We're reading an immutable global. It's fine.
if typeface.isNone:
typeface = some(parseTtf(fontAssets.getAsset(
"assets/fonts/Ubuntu-Regular_1.ttf")))
Expand Down
4 changes: 2 additions & 2 deletions frameos/src/scenes/default.nim
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ proc init*(frameConfig: FrameConfig, logger: Logger, dispatchEvent: proc(
fontSize: 32.0, borderColor: parseHtmlColor("#000000"), borderWidth: 1))
scene.node4 = unsplashApp.init(4.NodeId, scene, unsplashApp.AppConfig(keyword: "bird", cacheSeconds: 60.0))
scene.node6 = nodeApp8.init(6.NodeId, scene, nodeApp8.AppConfig(text: ""))
scene.node7 = qrApp.init(7.NodeId, scene, qrApp.AppConfig(code: "", padding: 4.0, position: "center-left",
size: 100.0, sizeUnit: "% of smallest dimension", offsetX: 0.0, offsetY: 0.0, qrCodeColor: parseHtmlColor(
scene.node7 = qrApp.init(7.NodeId, scene, qrApp.AppConfig(code: "", padding: 4, position: "center-left",
size: 100.0, sizeUnit: "% of smallest dimension", offsetX: 0, offsetY: 0, qrCodeColor: parseHtmlColor(
"#000000"), backgroundColor: parseHtmlColor("#ffffff")))
runEvent(scene, context)

Expand Down
Loading

0 comments on commit 1efe80c

Please sign in to comment.