Init
25
.gitignore
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# This file is for zig-specific build artifacts.
|
||||||
|
# If you have OS-specific or editor-specific files to ignore,
|
||||||
|
# such as *.swp or .DS_Store, put those in your global
|
||||||
|
# ~/.gitignore and put this in your ~/.gitconfig:
|
||||||
|
#
|
||||||
|
# [core]
|
||||||
|
# excludesfile = ~/.gitignore
|
||||||
|
#
|
||||||
|
# Cheers!
|
||||||
|
# -andrewrk
|
||||||
|
|
||||||
|
.zig-cache/
|
||||||
|
zig-out/
|
||||||
|
/release/
|
||||||
|
/debug/
|
||||||
|
/build/
|
||||||
|
/build-*/
|
||||||
|
/docgen_tmp/
|
||||||
|
|
||||||
|
# Although this was renamed to .zig-cache, let's leave it here for a few
|
||||||
|
# releases to make it less annoying to work with multiple branches.
|
||||||
|
zig-cache/
|
||||||
|
|
||||||
|
.envrc
|
||||||
|
.direnv
|
165
build.zig
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const protobuf = @import("protobuf");
|
||||||
|
const kindle = @import("kindle");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) !void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
const server_step = b.step("server", "build server");
|
||||||
|
const client_step = b.step("client", "build client");
|
||||||
|
const render_step = b.step("render", "build render application");
|
||||||
|
|
||||||
|
b.default_step.dependOn(server_step);
|
||||||
|
b.default_step.dependOn(client_step);
|
||||||
|
b.default_step.dependOn(render_step);
|
||||||
|
|
||||||
|
const clap_dep = b.dependency("clap", .{});
|
||||||
|
const zap_dep = b.dependency("zap", .{});
|
||||||
|
const protobuf_dep = b.dependency("protobuf", .{});
|
||||||
|
const zigimg_dep = b.dependency("zigimg", .{});
|
||||||
|
const wardrobe_dep = b.dependency("wardrobe", .{});
|
||||||
|
const temp_dep = b.dependency("temp", .{});
|
||||||
|
const zdt_dep = b.dependency("zdt", .{});
|
||||||
|
|
||||||
|
const gen_proto = b.step("gen-proto", "generates zig files from protocol buffer definitions");
|
||||||
|
const protc_step = protobuf.RunProtocStep.create(b, protobuf_dep.builder, target, .{
|
||||||
|
.destination_directory = b.path("src/eink_feed_protocol/proto"),
|
||||||
|
.source_files = &.{
|
||||||
|
"protocol/eink_feed.proto",
|
||||||
|
},
|
||||||
|
.include_directories = &.{},
|
||||||
|
});
|
||||||
|
gen_proto.dependOn(&protc_step.step);
|
||||||
|
|
||||||
|
const protocol = blk: {
|
||||||
|
const mod = b.addModule("eink_feed_protocol", .{
|
||||||
|
.root_source_file = b.path("src/eink_feed_protocol/root.zig"),
|
||||||
|
});
|
||||||
|
mod.addImport("protobuf", protobuf_dep.module("protobuf"));
|
||||||
|
|
||||||
|
break :blk mod;
|
||||||
|
};
|
||||||
|
|
||||||
|
const server_mod = b.addModule("eink_feed_server", .{
|
||||||
|
.root_source_file = b.path("src/eink_feed_server/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
});
|
||||||
|
server_mod.addImport("eink_feed_protocol", protocol);
|
||||||
|
server_mod.addImport("zap", zap_dep.module("zap"));
|
||||||
|
server_mod.addImport("zigimg", zigimg_dep.module("zigimg"));
|
||||||
|
|
||||||
|
{
|
||||||
|
const server_exe = b.addExecutable(.{
|
||||||
|
.name = "eink-feed-server",
|
||||||
|
.root_source_file = b.path("src/eink_feed_server/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
server_exe.root_module.addImport("eink_feed_server", server_mod);
|
||||||
|
|
||||||
|
server_exe.root_module.addImport("clap", clap_dep.module("clap"));
|
||||||
|
|
||||||
|
const server_exe_install = b.addInstallArtifact(server_exe, .{});
|
||||||
|
server_step.dependOn(&server_exe_install.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const libhoudini = buildLibhoudinie(b, target, optimize);
|
||||||
|
|
||||||
|
const render_mod = b.addModule("eink_feed_render", .{
|
||||||
|
.root_source_file = b.path("src/eink_feed_render/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
});
|
||||||
|
render_mod.addImport("eink_feed_server", server_mod);
|
||||||
|
render_mod.addImport("eink_feed_protocol", protocol);
|
||||||
|
render_mod.addImport("zigimg", zigimg_dep.module("zigimg"));
|
||||||
|
render_mod.addImport("wardrobe", wardrobe_dep.module("wardrobe"));
|
||||||
|
render_mod.addImport("temp", temp_dep.module("temp"));
|
||||||
|
render_mod.addImport("zdt", zdt_dep.module("zdt"));
|
||||||
|
|
||||||
|
render_mod.linkLibrary(libhoudini);
|
||||||
|
render_mod.linkSystemLibrary("wkhtmltox", .{});
|
||||||
|
|
||||||
|
const departures_exe = b.addExecutable(.{
|
||||||
|
.name = "eink-feed-render-departures",
|
||||||
|
.root_source_file = b.path("src/eink_feed_render/main/departures.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
departures_exe.root_module.addImport("eink_feed_render", render_mod);
|
||||||
|
departures_exe.root_module.addImport("clap", clap_dep.module("clap"));
|
||||||
|
departures_exe.root_module.addImport("zdt", zdt_dep.module("zdt"));
|
||||||
|
departures_exe.linkLibC();
|
||||||
|
|
||||||
|
const render_exe_install = b.addInstallArtifact(departures_exe, .{});
|
||||||
|
render_step.dependOn(&render_exe_install.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const kindle_target = b.resolveTargetQuery(kindle.kindle_target_query_musl);
|
||||||
|
const kindle_dep = b.dependency("kindle", .{});
|
||||||
|
|
||||||
|
const libzlib = buildLibzlib(b, kindle_target, optimize);
|
||||||
|
const libcurl = try buildKindleLibcurl(b, kindle_target, optimize, libzlib);
|
||||||
|
const libevdev = buildLibevdev(b, kindle_target, optimize);
|
||||||
|
|
||||||
|
const client_mod = b.addModule("eink_feed_client", .{
|
||||||
|
.root_source_file = b.path("src/eink_feed_client/root.zig"),
|
||||||
|
.target = kindle_target,
|
||||||
|
});
|
||||||
|
client_mod.addImport("zigimg", zigimg_dep.module("zigimg"));
|
||||||
|
|
||||||
|
const client_exe = b.addExecutable(.{
|
||||||
|
.name = "eink-feed-client",
|
||||||
|
.root_source_file = b.path("src/eink_feed_client/main.zig"),
|
||||||
|
.target = kindle_target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
// client_exe.root_module.single_threaded = true; // won't run on kindle when enabling threading
|
||||||
|
client_exe.root_module.addImport("eink_feed_client", client_mod);
|
||||||
|
client_exe.root_module.addImport("zigimg", zigimg_dep.module("zigimg"));
|
||||||
|
client_exe.root_module.addImport("clap", clap_dep.module("clap"));
|
||||||
|
|
||||||
|
// client_exe.addLibraryPath(b.path("kindle/sysroot/lib"));
|
||||||
|
// client_exe.linkSystemLibrary2("c", .{ .preferred_link_mode = .dynamic });
|
||||||
|
// client_exe.addIncludePath(b.path("kindle/sysroot/usr/include"));
|
||||||
|
|
||||||
|
// client_exe.linkLibrary(libgetauxval_backport);
|
||||||
|
|
||||||
|
client_exe.root_module.addImport("kindle", kindle_dep.module("kindle"));
|
||||||
|
|
||||||
|
client_exe.root_module.addImport("eink_feed_protocol", protocol);
|
||||||
|
|
||||||
|
client_exe.linkLibrary(libcurl);
|
||||||
|
client_exe.linkLibrary(libevdev);
|
||||||
|
|
||||||
|
const client_exe_install = b.addInstallArtifact(client_exe, .{});
|
||||||
|
client_step.dependOn(&client_exe_install.step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildLibzlib(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.Compile {
|
||||||
|
const libzlib = @import("libs/zlib.zig").create(b, target, optimize);
|
||||||
|
return libzlib;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildKindleLibcurl(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, libzlib: *std.Build.Step.Compile) !*std.Build.Step.Compile {
|
||||||
|
const libmbedtls = @import("libs/mbedtls.zig").create(b, target, optimize);
|
||||||
|
const libcurl = try @import("libs/curl.zig").create(b, target, optimize);
|
||||||
|
libcurl.linkLibrary(libmbedtls);
|
||||||
|
libcurl.linkLibrary(libzlib);
|
||||||
|
|
||||||
|
return libcurl;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildLibevdev(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.Compile {
|
||||||
|
const libevdev = @import("libs/evdev/root.zig").create(b, target, optimize);
|
||||||
|
return libevdev;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildLibhoudinie(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.Compile {
|
||||||
|
const libhoudini = @import("libs/houdini.zig").create(b, target, optimize);
|
||||||
|
return libhoudini;
|
||||||
|
}
|
66
build.zig.zon
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
.{
|
||||||
|
.name = .eink_feed,
|
||||||
|
.version = "0.0.0",
|
||||||
|
.fingerprint = 0x3ea460595d6d91ce,
|
||||||
|
.minimum_zig_version = "0.14.0",
|
||||||
|
.dependencies = .{
|
||||||
|
.clap = .{
|
||||||
|
.url = "git+https://github.com/Hejsil/zig-clap?ref=0.10.0#e47028deaefc2fb396d3d9e9f7bd776ae0b2a43a",
|
||||||
|
.hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0",
|
||||||
|
},
|
||||||
|
.zap = .{
|
||||||
|
.url = "git+https://github.com/zigzap/zap?ref=v0.10.1#8d187310c7ee4f86faa7ef0ecdac0bf747216dea",
|
||||||
|
.hash = "zap-0.9.1-GoeB84M8JACjZKDNq2LA5hB24Z-ZrZ_HUKRXd8qxL2JW",
|
||||||
|
},
|
||||||
|
.protobuf = .{
|
||||||
|
.url = "git+https://github.com/Arwalk/zig-protobuf#77161cbf10625fea90ee3bf6eee47e6b8c587845",
|
||||||
|
.hash = "protobuf-2.0.0-0e82akObGwBZQtrB7Qb6CTWSrwYKRPJ0M4L0CuTJmJ9G",
|
||||||
|
},
|
||||||
|
.curl = .{
|
||||||
|
.url = "https://github.com/curl/curl/releases/download/curl-8_12_1/curl-8.12.1.tar.xz",
|
||||||
|
.hash = "12203302a349824d4200886559735a236880c6a52f065e9876984b8fd671cbbe3e4d",
|
||||||
|
},
|
||||||
|
.mbedtls = .{
|
||||||
|
.url = "https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/mbedtls-3.6.2.tar.gz",
|
||||||
|
.hash = "1220df23c1e89d301f43026518f90e75cbfd91d92f9e62e5243268eec1db1b425db1",
|
||||||
|
},
|
||||||
|
.zlib = .{
|
||||||
|
.url = "https://github.com/madler/zlib/archive/refs/tags/v1.3.1.tar.gz",
|
||||||
|
.hash = "1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb",
|
||||||
|
},
|
||||||
|
.zigimg = .{
|
||||||
|
.url = "git+https://github.com/zigimg/zigimg.git?ref=zigimg_zig_0.14.1#7144f698850afd968fa87e04b95343d819912a33",
|
||||||
|
.hash = "zigimg-0.1.0-8_eo2h6XEgAR6UkBiCtWECcml0gtx0oIXE3MX7sPCZof",
|
||||||
|
},
|
||||||
|
.libevdev = .{
|
||||||
|
.url = "git+https://gitlab.freedesktop.org/libevdev/libevdev#libevdev-1.13.4",
|
||||||
|
.hash = "N-V-__8AANMxDAAf-rkutac-3EWwxzHeShacbZc9_8wshGYa",
|
||||||
|
},
|
||||||
|
.kindle = .{
|
||||||
|
.url = "git+https://git.dergrimm.net/dergrimm/kindle#ebb69f996082588b24c1533b93a4c2e6cb42314c",
|
||||||
|
.hash = "kindle-0.0.0-F0nCak6GDwCttE5X824ZCKFO46h_grOQqRBMkpLhwPcQ",
|
||||||
|
},
|
||||||
|
.wardrobe = .{
|
||||||
|
.url = "git+https://github.com/edqx/wardrobe.git#3895b779d4605f97ed3c8fee1c4982e0f80d4655",
|
||||||
|
.hash = "wardrobe-0.0.1-Hd1QO7BgAAAM9mdmUrWUnT8Hv5BcsEBLvfT6-N5N1KMB",
|
||||||
|
},
|
||||||
|
.temp = .{
|
||||||
|
.url = "git+https://github.com/abhinav/temp.zig.git#382a711f253d54a88f222650e6c03b62411373c6",
|
||||||
|
.hash = "temp-0.3.0-Ogl1FSSBAAB2WXv3QbTYO3NYfVFoYTBkZsVK3MYvbxFE",
|
||||||
|
},
|
||||||
|
.houdini = .{
|
||||||
|
.url = "git+https://github.com/vmg/houdini.git#59727b85553b70d468743076219e620d6c0d3cad",
|
||||||
|
.hash = "N-V-__8AANB3AQBMlbE5QkMkCCuMvinkImFwpUdLElvE8MDh",
|
||||||
|
},
|
||||||
|
.zdt = .{
|
||||||
|
.url = "git+https://codeberg.org/FObersteiner/zdt.git?ref=v0.6.9#495455d3baa0852579a230bc44f64c648904448f",
|
||||||
|
.hash = "zdt-0.6.9-xr0_vCyGDwCPx6axnAhidD9KYY2ocBg7mqBVqWNrkhIw",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"src",
|
||||||
|
"libs",
|
||||||
|
},
|
||||||
|
}
|
19
config/server.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"channels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "test-channel",
|
||||||
|
"display": {
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"orientation": "landscape_left"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"name": "test-kindle",
|
||||||
|
"channel_id": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
147
flake.lock
generated
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696426674,
|
||||||
|
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils_2": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1705309234,
|
||||||
|
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1749995256,
|
||||||
|
"narHash": "sha256-LEGfcombb0otUf23oAmYCXR4+lMQKa49XmU0G5HItGI=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "daa45f10955cc2207ac9c5f0206774d2f757c162",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-24.11",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1708161998,
|
||||||
|
"narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "84d981bae8b5e783b3b548de505b22880559515f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-23.11",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"zig": "zig"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zig": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-utils": "flake-utils_2",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1750293335,
|
||||||
|
"narHash": "sha256-qAX+hIcUqVl8BzLHTRBrceF2vGeht+gfhvvUZgKe/98=",
|
||||||
|
"owner": "mitchellh",
|
||||||
|
"repo": "zig-overlay",
|
||||||
|
"rev": "3453b39a83069811d077a0bc696849c6907112d5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "mitchellh",
|
||||||
|
"repo": "zig-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
34
flake.nix
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
|
||||||
|
flake-utils.url = "github:/numtide/flake-utils";
|
||||||
|
zig.url = "github:mitchellh/zig-overlay";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
zig,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
overlays = [ ];
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system overlays;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
with pkgs;
|
||||||
|
{
|
||||||
|
devShells.default = mkShell {
|
||||||
|
buildInputs = [
|
||||||
|
librsvg
|
||||||
|
wkhtmltopdf
|
||||||
|
zig.packages.${system}."0.14.1"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
534
libs/curl.zig
Normal file
|
@ -0,0 +1,534 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn create(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) !*std.Build.Step.Compile {
|
||||||
|
const lib = b.addStaticLibrary(.{
|
||||||
|
.name = "curl",
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.linkLibC();
|
||||||
|
|
||||||
|
const curl_dep = b.dependency("curl", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.addCSourceFiles(.{
|
||||||
|
.root = curl_dep.path(""),
|
||||||
|
.files = srcs,
|
||||||
|
.flags = &.{"-std=gnu89"},
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.addIncludePath(curl_dep.path("lib"));
|
||||||
|
lib.addIncludePath(curl_dep.path("include"));
|
||||||
|
lib.installHeadersDirectory(curl_dep.path("include/curl"), "curl", .{});
|
||||||
|
|
||||||
|
lib.root_module.addCMacro("BUILDING_LIBCURL", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_STATICLIB", "1");
|
||||||
|
|
||||||
|
lib.root_module.addCMacro("USE_MBEDTLS", "1");
|
||||||
|
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_COOKIES", "1"); // fix sterrror_r confusion
|
||||||
|
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_DICT", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_GOPHER", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_HSTS", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_IMAP", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_LDAP", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_LDAPS", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_MQTT", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_POP3", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_RTSP", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_SMTP", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_TELNET", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_DISABLE_TFTP", "1");
|
||||||
|
lib.root_module.addCMacro("CURL_EXTERN_SYMBOL", "__attribute__((__visibility__(\"default\")))");
|
||||||
|
lib.root_module.addCMacro("CURL_OS", "\"armv7-unknown-linux-gnueabihf\"");
|
||||||
|
lib.root_module.addCMacro("GETHOSTNAME_TYPE_ARG2", "unsigned int");
|
||||||
|
lib.root_module.addCMacro("HAVE_ALARM", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_ARPA_INET_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_ATOMIC", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_BASENAME", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_BOOL_T", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC_RAW", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_DECL_FSEEKO", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_DIRENT_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_DLFCN_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_EVENTFD", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_FCNTL", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_FCNTL_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_FCNTL_O_NONBLOCK", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_FNMATCH", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_FREEADDRINFO", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_FSEEKO", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_FSETXATTR", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_FSETXATTR_5", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_FTRUNCATE", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETADDRINFO", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETADDRINFO_THREADSAFE", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETEUID", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETHOSTBYNAME_R", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETHOSTBYNAME_R_6", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETHOSTNAME", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETIFADDRS", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETPEERNAME", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETPPID", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETPWUID", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETPWUID_R", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETRLIMIT", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETSOCKNAME", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GETTIMEOFDAY", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_POSIX_STRERROR_R", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_GMTIME_R", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_IFADDRS_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_IF_NAMETOINDEX", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_INET_NTOP", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_INET_PTON", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_INTTYPES_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_IOCTL_FIONBIO", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_IOCTL_SIOCGIFADDR", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_LIBGEN_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_LINUX_TCP_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_LOCALE_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_LONGLONG", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_MEMRCHR", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_MSG_NOSIGNAL", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_NETDB_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_NETINET_IN_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_NETINET_TCP_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_NETINET_UDP_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_NET_IF_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_OPENDIR", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_PIPE", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_POLL", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_POLL_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_PTHREAD_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_PWD_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_REALPATH", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_RECV", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SA_FAMILY_T", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SCHED_YIELD", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SELECT", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SEND", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SENDMSG", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SETLOCALE", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SETRLIMIT", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SIGACTION", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SIGINTERRUPT", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SIGNAL", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SIGSETJMP", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SNPRINTF", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SOCKET", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SOCKETPAIR", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STDATOMIC_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STDBOOL_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STDINT_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STDIO_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STDLIB_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STRCASECMP", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STRDUP", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STRERROR_R", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STRINGS_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STRING_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STRTOK_R", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STRTOLL", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STRUCT_SOCKADDR_STORAGE", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_STRUCT_TIMEVAL", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SUSECONDS_T", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_EVENTFD_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_IOCTL_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_PARAM_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_POLL_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_RESOURCE_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_SELECT_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_SOCKET_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_STAT_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_TIME_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_TYPES_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_UN_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_SYS_XATTR_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_TERMIOS_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_TERMIO_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_UNISTD_H", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_UTIME", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_UTIMES", "1");
|
||||||
|
lib.root_module.addCMacro("HAVE_UTIME_H", "1");
|
||||||
|
lib.root_module.addCMacro("LT_OBJDIR", "\".libs/\"");
|
||||||
|
lib.root_module.addCMacro("PACKAGE", "\"curl\"");
|
||||||
|
lib.root_module.addCMacro("PACKAGE_BUGREPORT", "\"a suitable curl mailing list: https://curl.se/mail/\"");
|
||||||
|
lib.root_module.addCMacro("PACKAGE_NAME", "\"curl\"");
|
||||||
|
lib.root_module.addCMacro("PACKAGE_STRING", "\"curl -\"");
|
||||||
|
lib.root_module.addCMacro("PACKAGE_TARNAME", "\"curl\"");
|
||||||
|
lib.root_module.addCMacro("PACKAGE_URL", "\"\"");
|
||||||
|
lib.root_module.addCMacro("PACKAGE_VERSION", "\" - \"");
|
||||||
|
lib.root_module.addCMacro("SIZEOF_CURL_OFF_T", "8");
|
||||||
|
lib.root_module.addCMacro("SIZEOF_CURL_SOCKET_T", "4");
|
||||||
|
lib.root_module.addCMacro("SIZEOF_INT", "4");
|
||||||
|
lib.root_module.addCMacro("SIZEOF_LONG", "4");
|
||||||
|
lib.root_module.addCMacro("SIZEOF_OFF_T", "8");
|
||||||
|
lib.root_module.addCMacro("SIZEOF_SIZE_T", "4");
|
||||||
|
lib.root_module.addCMacro("SIZEOF_TIME_T", "4");
|
||||||
|
lib.root_module.addCMacro("STDC_HEADERS", "1");
|
||||||
|
lib.root_module.addCMacro("USE_IPV6", "1");
|
||||||
|
lib.root_module.addCMacro("USE_THREADS_POSIX", "1");
|
||||||
|
lib.root_module.addCMacro("USE_UNIX_SOCKETS", "1");
|
||||||
|
lib.root_module.addCMacro("VERSION", "\"-\"");
|
||||||
|
lib.root_module.addCMacro("_FILE_OFFSET_BITS", "64");
|
||||||
|
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_LDAP", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_LDAPS", "1");
|
||||||
|
// lib.root_module.addCMacro("USE_MBEDTLS", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_DICT", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_FILE", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_FTP", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_GOPHER", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_IMAP", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_MQTT", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_POP3", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_RTSP", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_SMB", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_SMTP", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_TELNET", "1");
|
||||||
|
// lib.root_module.addCMacro("CURL_DISABLE_TFTP", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_LIBZ", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_ZLIB_H", "1");
|
||||||
|
|
||||||
|
// lib.root_module.addCMacro("CURL_EXTERN_SYMBOL", "__attribute__ ((__visibility__ (\"default\"))");
|
||||||
|
|
||||||
|
// lib.root_module.addCMacro("ENABLE_IPV6", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_ALARM", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_ALLOCA_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_ARPA_INET_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_ARPA_TFTP_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_ASSERT_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_BASENAME", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_BOOL_T", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_DLFCN_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_ERRNO_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_FCNTL", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_FCNTL_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_FCNTL_O_NONBLOCK", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_FREEADDRINFO", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_FTRUNCATE", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETADDRINFO", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETEUID", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETPPID", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETHOSTBYNAME", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETHOSTBYNAME_R", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETHOSTBYNAME_R_6", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETHOSTNAME", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETPPID", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETPROTOBYNAME", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETPEERNAME", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETSOCKNAME", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_IF_NAMETOINDEX", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETPWUID", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETPWUID_R", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETRLIMIT", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GETTIMEOFDAY", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_GMTIME_R", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_IFADDRS_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_INET_ADDR", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_INET_PTON", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SA_FAMILY_T", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_INTTYPES_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_IOCTL", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_IOCTL_FIONBIO", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_IOCTL_SIOCGIFADDR", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_LDAP_URL_PARSE", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_LIBGEN_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_IDN2_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_LL", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_LOCALE_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_LOCALTIME_R", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_LONGLONG", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_MALLOC_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_MEMORY_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_MSG_NOSIGNAL", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_NETDB_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_NETINET_IN_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_NETINET_TCP_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_LINUX_TCP_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_NET_IF_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_PIPE", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_POLL", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_POLL_FINE", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_POLL_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_POSIX_STRERROR_R", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_PTHREAD_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_PWD_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_RECV", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SELECT", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SEND", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_FSETXATTR", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_FSETXATTR_5", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SETJMP_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SETLOCALE", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SETRLIMIT", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SETSOCKOPT", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SIGACTION", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SIGINTERRUPT", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SIGNAL", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SIGNAL_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SIGSETJMP", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SOCKET", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STDBOOL_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STDINT_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STDIO_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STDLIB_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STRCASECMP", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STRDUP", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STRERROR_R", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STRINGS_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STRING_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STRSTR", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STRTOK_R", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STRTOLL", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STRUCT_SOCKADDR_STORAGE", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_STRUCT_TIMEVAL", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_IOCTL_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_PARAM_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_POLL_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_RESOURCE_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_SELECT_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_SOCKET_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_STAT_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_TIME_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_TYPES_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_UIO_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_SYS_UN_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_TERMIOS_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_TERMIO_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_TIME_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_UNAME", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_UNISTD_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_UTIME", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_UTIMES", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_UTIME_H", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_VARIADIC_MACROS_C99", "1");
|
||||||
|
// lib.root_module.addCMacro("HAVE_VARIADIC_MACROS_GCC", "1");
|
||||||
|
// lib.root_module.addCMacro("OS", "\"Linux\"");
|
||||||
|
// lib.root_module.addCMacro("RANDOM_FILE", "\"/dev/urandom\"");
|
||||||
|
// lib.root_module.addCMacro("RECV_TYPE_ARG1", "int");
|
||||||
|
// lib.root_module.addCMacro("RECV_TYPE_ARG2", "void *");
|
||||||
|
// lib.root_module.addCMacro("RECV_TYPE_ARG3", "size_t");
|
||||||
|
// lib.root_module.addCMacro("RECV_TYPE_ARG4", "int");
|
||||||
|
// lib.root_module.addCMacro("RECV_TYPE_RETV", "ssize_t");
|
||||||
|
// lib.root_module.addCMacro("SEND_QUAL_ARG2", "const");
|
||||||
|
// lib.root_module.addCMacro("SEND_TYPE_ARG1", "int");
|
||||||
|
// lib.root_module.addCMacro("SEND_TYPE_ARG2", "void *");
|
||||||
|
// lib.root_module.addCMacro("SEND_TYPE_ARG3", "size_t");
|
||||||
|
// lib.root_module.addCMacro("SEND_TYPE_ARG4", "int");
|
||||||
|
// lib.root_module.addCMacro("SEND_TYPE_RETV", "ssize_t");
|
||||||
|
|
||||||
|
// var buf: [2]u8 = undefined;
|
||||||
|
|
||||||
|
// lib.root_module.addCMacro(
|
||||||
|
// "SIZEOF_INT",
|
||||||
|
// try std.fmt.bufPrint(&buf, "{d}", .{target.result.c_type_byte_size(.int)}),
|
||||||
|
// );
|
||||||
|
// lib.root_module.addCMacro(
|
||||||
|
// "SIZEOF_SHORT",
|
||||||
|
// try std.fmt.bufPrint(&buf, "{d}", .{target.result.c_type_byte_size(.short)}),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const long = try std.fmt.bufPrint(&buf, "{d}", .{target.result.c_type_byte_size(.long)});
|
||||||
|
// lib.root_module.addCMacro(
|
||||||
|
// "SIZEOF_LONG",
|
||||||
|
// long,
|
||||||
|
// );
|
||||||
|
// lib.root_module.addCMacro("SIZEOF_OFF_T", "8");
|
||||||
|
// lib.root_module.addCMacro("SIZEOF_CURL_OFF_T", "8");
|
||||||
|
// lib.root_module.addCMacro("SIZEOF_SIZE_T", "8");
|
||||||
|
// lib.root_module.addCMacro("SIZEOF_TIME_T", "8");
|
||||||
|
|
||||||
|
// lib.root_module.addCMacro("STDC_HEADERS", "1");
|
||||||
|
// lib.root_module.addCMacro("TIME_WITH_SYS_TIME", "1");
|
||||||
|
// lib.root_module.addCMacro("USE_THREADS_POSIX", "1");
|
||||||
|
// lib.root_module.addCMacro("USE_UNIX_SOCKETS", "1");
|
||||||
|
// lib.root_module.addCMacro("_FILE_OFFSET_BITS", "64");
|
||||||
|
|
||||||
|
return lib;
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcs = &.{
|
||||||
|
"lib/ws.c",
|
||||||
|
"lib/warnless.c",
|
||||||
|
"lib/vtls/x509asn1.c",
|
||||||
|
"lib/vtls/wolfssl.c",
|
||||||
|
"lib/vtls/vtls_spack.c",
|
||||||
|
"lib/vtls/vtls_scache.c",
|
||||||
|
"lib/vtls/vtls.c",
|
||||||
|
"lib/vtls/sectransp.c",
|
||||||
|
"lib/vtls/schannel_verify.c",
|
||||||
|
"lib/vtls/schannel.c",
|
||||||
|
"lib/vtls/rustls.c",
|
||||||
|
"lib/vtls/openssl.c",
|
||||||
|
"lib/vtls/mbedtls_threadlock.c",
|
||||||
|
"lib/vtls/mbedtls.c",
|
||||||
|
"lib/vtls/keylog.c",
|
||||||
|
"lib/vtls/hostcheck.c",
|
||||||
|
"lib/vtls/gtls.c",
|
||||||
|
"lib/vtls/cipher_suite.c",
|
||||||
|
"lib/vtls/bearssl.c",
|
||||||
|
"lib/vssh/wolfssh.c",
|
||||||
|
"lib/vssh/libssh2.c",
|
||||||
|
"lib/vssh/libssh.c",
|
||||||
|
"lib/vssh/curl_path.c",
|
||||||
|
"lib/vquic/vquic.c",
|
||||||
|
"lib/vquic/vquic-tls.c",
|
||||||
|
"lib/vquic/curl_quiche.c",
|
||||||
|
"lib/vquic/curl_osslq.c",
|
||||||
|
"lib/vquic/curl_ngtcp2.c",
|
||||||
|
"lib/vquic/curl_msh3.c",
|
||||||
|
"lib/version_win32.c",
|
||||||
|
"lib/version.c",
|
||||||
|
"lib/vauth/vauth.c",
|
||||||
|
"lib/vauth/spnego_sspi.c",
|
||||||
|
"lib/vauth/spnego_gssapi.c",
|
||||||
|
"lib/vauth/oauth2.c",
|
||||||
|
"lib/vauth/ntlm_sspi.c",
|
||||||
|
"lib/vauth/ntlm.c",
|
||||||
|
"lib/vauth/krb5_sspi.c",
|
||||||
|
"lib/vauth/krb5_gssapi.c",
|
||||||
|
"lib/vauth/gsasl.c",
|
||||||
|
"lib/vauth/digest_sspi.c",
|
||||||
|
"lib/vauth/digest.c",
|
||||||
|
"lib/vauth/cram.c",
|
||||||
|
"lib/vauth/cleartext.c",
|
||||||
|
"lib/urlapi.c",
|
||||||
|
"lib/url.c",
|
||||||
|
"lib/transfer.c",
|
||||||
|
"lib/timeval.c",
|
||||||
|
"lib/timediff.c",
|
||||||
|
"lib/tftp.c",
|
||||||
|
"lib/telnet.c",
|
||||||
|
"lib/system_win32.c",
|
||||||
|
"lib/strtoofft.c",
|
||||||
|
"lib/strtok.c",
|
||||||
|
"lib/strparse.c",
|
||||||
|
"lib/strerror.c",
|
||||||
|
"lib/strdup.c",
|
||||||
|
"lib/strcase.c",
|
||||||
|
"lib/splay.c",
|
||||||
|
"lib/speedcheck.c",
|
||||||
|
"lib/socks_sspi.c",
|
||||||
|
"lib/socks_gssapi.c",
|
||||||
|
"lib/socks.c",
|
||||||
|
"lib/socketpair.c",
|
||||||
|
"lib/smtp.c",
|
||||||
|
"lib/smb.c",
|
||||||
|
"lib/slist.c",
|
||||||
|
"lib/share.c",
|
||||||
|
"lib/sha256.c",
|
||||||
|
"lib/setopt.c",
|
||||||
|
"lib/sendf.c",
|
||||||
|
"lib/select.c",
|
||||||
|
"lib/rtsp.c",
|
||||||
|
"lib/request.c",
|
||||||
|
"lib/rename.c",
|
||||||
|
"lib/rand.c",
|
||||||
|
"lib/psl.c",
|
||||||
|
"lib/progress.c",
|
||||||
|
"lib/pop3.c",
|
||||||
|
"lib/pingpong.c",
|
||||||
|
"lib/parsedate.c",
|
||||||
|
"lib/openldap.c",
|
||||||
|
"lib/noproxy.c",
|
||||||
|
"lib/nonblock.c",
|
||||||
|
"lib/netrc.c",
|
||||||
|
"lib/multi.c",
|
||||||
|
"lib/mqtt.c",
|
||||||
|
"lib/mprintf.c",
|
||||||
|
"lib/mime.c",
|
||||||
|
"lib/memdebug.c",
|
||||||
|
"lib/md5.c",
|
||||||
|
"lib/md4.c",
|
||||||
|
"lib/macos.c",
|
||||||
|
"lib/llist.c",
|
||||||
|
"lib/ldap.c",
|
||||||
|
"lib/krb5.c",
|
||||||
|
"lib/inet_pton.c",
|
||||||
|
"lib/inet_ntop.c",
|
||||||
|
"lib/imap.c",
|
||||||
|
"lib/if2ip.c",
|
||||||
|
"lib/idn.c",
|
||||||
|
"lib/httpsrr.c",
|
||||||
|
"lib/http_proxy.c",
|
||||||
|
"lib/http_ntlm.c",
|
||||||
|
"lib/http_negotiate.c",
|
||||||
|
"lib/http_digest.c",
|
||||||
|
"lib/http_chunks.c",
|
||||||
|
"lib/http_aws_sigv4.c",
|
||||||
|
"lib/http2.c",
|
||||||
|
"lib/http1.c",
|
||||||
|
"lib/http.c",
|
||||||
|
"lib/hsts.c",
|
||||||
|
"lib/hostsyn.c",
|
||||||
|
"lib/hostip6.c",
|
||||||
|
"lib/hostip4.c",
|
||||||
|
"lib/hostip.c",
|
||||||
|
"lib/hostasyn.c",
|
||||||
|
"lib/hmac.c",
|
||||||
|
"lib/headers.c",
|
||||||
|
"lib/hash.c",
|
||||||
|
"lib/gopher.c",
|
||||||
|
"lib/getinfo.c",
|
||||||
|
"lib/getenv.c",
|
||||||
|
"lib/ftplistparser.c",
|
||||||
|
"lib/ftp.c",
|
||||||
|
"lib/formdata.c",
|
||||||
|
"lib/fopen.c",
|
||||||
|
"lib/fileinfo.c",
|
||||||
|
"lib/file.c",
|
||||||
|
"lib/escape.c",
|
||||||
|
"lib/easyoptions.c",
|
||||||
|
"lib/easygetopt.c",
|
||||||
|
"lib/easy.c",
|
||||||
|
"lib/dynhds.c",
|
||||||
|
"lib/dynbuf.c",
|
||||||
|
"lib/doh.c",
|
||||||
|
"lib/dllmain.c",
|
||||||
|
"lib/dict.c",
|
||||||
|
"lib/cw-out.c",
|
||||||
|
"lib/curl_trc.c",
|
||||||
|
"lib/curl_threads.c",
|
||||||
|
"lib/curl_sspi.c",
|
||||||
|
"lib/curl_sha512_256.c",
|
||||||
|
"lib/curl_sasl.c",
|
||||||
|
"lib/curl_rtmp.c",
|
||||||
|
"lib/curl_range.c",
|
||||||
|
"lib/curl_ntlm_core.c",
|
||||||
|
"lib/curl_multibyte.c",
|
||||||
|
"lib/curl_memrchr.c",
|
||||||
|
"lib/curl_gssapi.c",
|
||||||
|
"lib/curl_gethostname.c",
|
||||||
|
"lib/curl_get_line.c",
|
||||||
|
"lib/curl_fnmatch.c",
|
||||||
|
"lib/curl_endian.c",
|
||||||
|
"lib/curl_des.c",
|
||||||
|
"lib/curl_addrinfo.c",
|
||||||
|
"lib/cookie.c",
|
||||||
|
"lib/content_encoding.c",
|
||||||
|
"lib/connect.c",
|
||||||
|
"lib/conncache.c",
|
||||||
|
"lib/cfilters.c",
|
||||||
|
"lib/cf-socket.c",
|
||||||
|
"lib/cf-https-connect.c",
|
||||||
|
"lib/cf-haproxy.c",
|
||||||
|
"lib/cf-h2-proxy.c",
|
||||||
|
"lib/cf-h1-proxy.c",
|
||||||
|
"lib/bufref.c",
|
||||||
|
"lib/bufq.c",
|
||||||
|
"lib/base64.c",
|
||||||
|
"lib/asyn-thread.c",
|
||||||
|
"lib/asyn-ares.c",
|
||||||
|
"lib/amigaos.c",
|
||||||
|
"lib/altsvc.c",
|
||||||
|
};
|
31
libs/evdev/capture_out.zig
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
//! Capture stdout and write it into the <file>.
|
||||||
|
//! Usage: capture_out <file> <command> [args...]
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
const allocator = std.heap.page_allocator;
|
||||||
|
|
||||||
|
var arg_iter = std.process.args();
|
||||||
|
defer arg_iter.deinit();
|
||||||
|
|
||||||
|
_ = arg_iter.next();
|
||||||
|
const outpath = arg_iter.next() orelse unreachable;
|
||||||
|
|
||||||
|
var args = std.ArrayList([]const u8).init(allocator);
|
||||||
|
defer args.deinit();
|
||||||
|
while (arg_iter.next()) |arg| try args.append(arg);
|
||||||
|
|
||||||
|
const res = try std.process.Child.run(.{
|
||||||
|
.allocator = allocator,
|
||||||
|
.argv = args.items,
|
||||||
|
.max_output_bytes = std.math.maxInt(usize),
|
||||||
|
});
|
||||||
|
|
||||||
|
var out = try std.fs.cwd().createFile(outpath, .{});
|
||||||
|
defer out.close();
|
||||||
|
try out.writeAll(res.stdout);
|
||||||
|
try std.io.getStdErr().writer().writeAll(res.stderr);
|
||||||
|
|
||||||
|
std.process.exit(res.term.Exited);
|
||||||
|
}
|
74
libs/evdev/root.zig
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn create(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.Compile {
|
||||||
|
const lib = b.addStaticLibrary(.{
|
||||||
|
.name = "evdev",
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.linkLibC();
|
||||||
|
lib.linkSystemLibrary("rt");
|
||||||
|
|
||||||
|
const evdev_dep = b.dependency("libevdev", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.addConfigHeader(b.addConfigHeader(.{ .include_path = "config.h" }, .{
|
||||||
|
._GNU_SOURCE = 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const event_names_h = b: {
|
||||||
|
const run = b.addRunArtifact(b.addExecutable(.{
|
||||||
|
.name = "capture_out",
|
||||||
|
.root_source_file = b.path("libs/evdev/capture_out.zig"),
|
||||||
|
.target = b.graph.host,
|
||||||
|
}));
|
||||||
|
const out = run.addOutputFileArg("libevdev/event-names.h");
|
||||||
|
run.addFileArg(evdev_dep.path("libevdev/make-event-names.py"));
|
||||||
|
run.addFileInput(evdev_dep.path("libevdev/libevdev.h"));
|
||||||
|
const os = switch (target.result.os.tag) {
|
||||||
|
.linux => "linux",
|
||||||
|
.freebsd => "freebsd",
|
||||||
|
else => @panic("Unsupported OS"),
|
||||||
|
};
|
||||||
|
run.addFileArg(evdev_dep.path(b.fmt("include/linux/{s}/input.h", .{os})));
|
||||||
|
run.addFileArg(evdev_dep.path(b.fmt("include/linux/{s}/input-event-codes.h", .{os})));
|
||||||
|
break :b out;
|
||||||
|
};
|
||||||
|
|
||||||
|
lib.addIncludePath(event_names_h.dirname());
|
||||||
|
lib.addIncludePath(evdev_dep.path("."));
|
||||||
|
lib.addIncludePath(evdev_dep.path("include"));
|
||||||
|
|
||||||
|
const flags = &[_][]const u8{
|
||||||
|
// c_std=gnu99
|
||||||
|
"-std=gnu99",
|
||||||
|
// warning_level=2
|
||||||
|
"-Wall",
|
||||||
|
"-Wextra",
|
||||||
|
// cflags
|
||||||
|
"-Wno-unused-parameter",
|
||||||
|
"-fvisibility=hidden",
|
||||||
|
"-Wmissing-prototypes",
|
||||||
|
"-Wstrict-prototypes",
|
||||||
|
// disable UB sanitizer, which is enabled by default by Zig
|
||||||
|
"-fno-sanitize=undefined",
|
||||||
|
"-D_FORTIFY_SOURCE=0", // really really bad (!!!) but too lazy to backport __poll_chk (sys/poll.h) to kindle glibc
|
||||||
|
};
|
||||||
|
lib.addCSourceFiles(.{
|
||||||
|
.root = evdev_dep.path("."),
|
||||||
|
.flags = flags,
|
||||||
|
.files = &.{
|
||||||
|
"libevdev/libevdev-uinput.c",
|
||||||
|
"libevdev/libevdev.c",
|
||||||
|
"libevdev/libevdev-names.c",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.installHeader(evdev_dep.path("libevdev/libevdev.h"), "libevdev/libevdev.h");
|
||||||
|
lib.installHeader(evdev_dep.path("libevdev/libevdev-uinput.h"), "libevdev/libevdev-uinput.h");
|
||||||
|
|
||||||
|
return lib;
|
||||||
|
}
|
40
libs/houdini.zig
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn create(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.Compile {
|
||||||
|
const lib = b.addStaticLibrary(.{
|
||||||
|
.name = "houdini",
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.linkLibC();
|
||||||
|
|
||||||
|
const houdini_dep = b.dependency("houdini", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.addIncludePath(houdini_dep.path(""));
|
||||||
|
lib.addCSourceFiles(.{
|
||||||
|
.root = houdini_dep.path(""),
|
||||||
|
.files = srcs,
|
||||||
|
.flags = &.{},
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.installHeader(houdini_dep.path("houdini.h"), "houdini.h");
|
||||||
|
lib.installHeader(houdini_dep.path("buffer.h"), "buffer.h");
|
||||||
|
|
||||||
|
return lib;
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcs = &.{
|
||||||
|
"buffer.c",
|
||||||
|
"houdini_href_e.c",
|
||||||
|
"houdini_html_e.c",
|
||||||
|
"houdini_html_u.c",
|
||||||
|
"houdini_js_e.c",
|
||||||
|
"houdini_js_u.c",
|
||||||
|
"houdini_uri_e.c",
|
||||||
|
"houdini_uri_u.c",
|
||||||
|
"houdini_xml_e.c",
|
||||||
|
};
|
140
libs/mbedtls.zig
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn create(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.Compile {
|
||||||
|
const lib = b.addStaticLibrary(.{
|
||||||
|
.name = "mbedtls",
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.linkLibC();
|
||||||
|
|
||||||
|
const mbedtls_dep = b.dependency("mbedtls", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.addCSourceFiles(.{
|
||||||
|
.root = mbedtls_dep.path(""),
|
||||||
|
.files = srcs,
|
||||||
|
.flags = &.{"-std=c99"},
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.addIncludePath(mbedtls_dep.path("include"));
|
||||||
|
lib.addIncludePath(mbedtls_dep.path("library"));
|
||||||
|
lib.installHeadersDirectory(mbedtls_dep.path("include/mbedtls"), "mbedtls", .{});
|
||||||
|
lib.installHeadersDirectory(mbedtls_dep.path("include/psa"), "psa", .{});
|
||||||
|
|
||||||
|
return lib;
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcs = &.{
|
||||||
|
"library/x509write_csr.c",
|
||||||
|
"library/x509write_crt.c",
|
||||||
|
"library/x509write.c",
|
||||||
|
"library/x509_csr.c",
|
||||||
|
"library/x509_crt.c",
|
||||||
|
"library/x509_crl.c",
|
||||||
|
"library/x509_create.c",
|
||||||
|
"library/x509.c",
|
||||||
|
"library/version_features.c",
|
||||||
|
"library/version.c",
|
||||||
|
"library/timing.c",
|
||||||
|
"library/threading.c",
|
||||||
|
"library/ssl_tls13_server.c",
|
||||||
|
"library/ssl_tls13_keys.c",
|
||||||
|
"library/ssl_tls13_generic.c",
|
||||||
|
"library/ssl_tls13_client.c",
|
||||||
|
"library/ssl_tls12_server.c",
|
||||||
|
"library/ssl_tls12_client.c",
|
||||||
|
"library/ssl_tls.c",
|
||||||
|
"library/ssl_ticket.c",
|
||||||
|
"library/ssl_msg.c",
|
||||||
|
"library/ssl_debug_helpers_generated.c",
|
||||||
|
"library/ssl_cookie.c",
|
||||||
|
"library/ssl_client.c",
|
||||||
|
"library/ssl_ciphersuites.c",
|
||||||
|
"library/ssl_cache.c",
|
||||||
|
"library/sha512.c",
|
||||||
|
"library/sha3.c",
|
||||||
|
"library/sha256.c",
|
||||||
|
"library/sha1.c",
|
||||||
|
"library/rsa_alt_helpers.c",
|
||||||
|
"library/rsa.c",
|
||||||
|
"library/ripemd160.c",
|
||||||
|
"library/psa_util.c",
|
||||||
|
"library/psa_its_file.c",
|
||||||
|
"library/psa_crypto_storage.c",
|
||||||
|
"library/psa_crypto_slot_management.c",
|
||||||
|
"library/psa_crypto_se.c",
|
||||||
|
"library/psa_crypto_rsa.c",
|
||||||
|
"library/psa_crypto_pake.c",
|
||||||
|
"library/psa_crypto_mac.c",
|
||||||
|
"library/psa_crypto_hash.c",
|
||||||
|
"library/psa_crypto_ffdh.c",
|
||||||
|
"library/psa_crypto_ecp.c",
|
||||||
|
"library/psa_crypto_driver_wrappers_no_static.c",
|
||||||
|
"library/psa_crypto_client.c",
|
||||||
|
"library/psa_crypto_cipher.c",
|
||||||
|
"library/psa_crypto_aead.c",
|
||||||
|
"library/psa_crypto.c",
|
||||||
|
"library/poly1305.c",
|
||||||
|
"library/platform_util.c",
|
||||||
|
"library/platform.c",
|
||||||
|
"library/pkwrite.c",
|
||||||
|
"library/pkparse.c",
|
||||||
|
"library/pkcs7.c",
|
||||||
|
"library/pkcs5.c",
|
||||||
|
"library/pkcs12.c",
|
||||||
|
"library/pk_wrap.c",
|
||||||
|
"library/pk_ecc.c",
|
||||||
|
"library/pk.c",
|
||||||
|
"library/pem.c",
|
||||||
|
"library/padlock.c",
|
||||||
|
"library/oid.c",
|
||||||
|
"library/nist_kw.c",
|
||||||
|
"library/net_sockets.c",
|
||||||
|
"library/mps_trace.c",
|
||||||
|
"library/mps_reader.c",
|
||||||
|
"library/memory_buffer_alloc.c",
|
||||||
|
"library/md5.c",
|
||||||
|
"library/md.c",
|
||||||
|
"library/lms.c",
|
||||||
|
"library/lmots.c",
|
||||||
|
"library/hmac_drbg.c",
|
||||||
|
"library/hkdf.c",
|
||||||
|
"library/gcm.c",
|
||||||
|
"library/error.c",
|
||||||
|
"library/entropy_poll.c",
|
||||||
|
"library/entropy.c",
|
||||||
|
"library/ecp_curves_new.c",
|
||||||
|
"library/ecp_curves.c",
|
||||||
|
"library/ecp.c",
|
||||||
|
"library/ecjpake.c",
|
||||||
|
"library/ecdsa.c",
|
||||||
|
"library/ecdh.c",
|
||||||
|
"library/dhm.c",
|
||||||
|
"library/des.c",
|
||||||
|
"library/debug.c",
|
||||||
|
"library/ctr_drbg.c",
|
||||||
|
"library/constant_time.c",
|
||||||
|
"library/cmac.c",
|
||||||
|
"library/cipher_wrap.c",
|
||||||
|
"library/cipher.c",
|
||||||
|
"library/chachapoly.c",
|
||||||
|
"library/chacha20.c",
|
||||||
|
"library/ccm.c",
|
||||||
|
"library/camellia.c",
|
||||||
|
"library/block_cipher.c",
|
||||||
|
"library/bignum_mod_raw.c",
|
||||||
|
"library/bignum_mod.c",
|
||||||
|
"library/bignum_core.c",
|
||||||
|
"library/bignum.c",
|
||||||
|
"library/base64.c",
|
||||||
|
"library/asn1write.c",
|
||||||
|
"library/asn1parse.c",
|
||||||
|
"library/aria.c",
|
||||||
|
"library/aesni.c",
|
||||||
|
"library/aesce.c",
|
||||||
|
"library/aes.c",
|
||||||
|
};
|
45
libs/zlib.zig
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn create(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.Compile {
|
||||||
|
const lib = b.addStaticLibrary(.{
|
||||||
|
.name = "z",
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.linkLibC();
|
||||||
|
|
||||||
|
const zlib_dep = b.dependency("zlib", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.addCSourceFiles(.{
|
||||||
|
.root = zlib_dep.path(""),
|
||||||
|
.files = srcs,
|
||||||
|
.flags = &.{"-std=c89"},
|
||||||
|
});
|
||||||
|
|
||||||
|
lib.installHeader(zlib_dep.path("zlib.h"), "zlib.h");
|
||||||
|
lib.installHeader(zlib_dep.path("zconf.h"), "zconf.h");
|
||||||
|
|
||||||
|
return lib;
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcs = &.{
|
||||||
|
"adler32.c",
|
||||||
|
"compress.c",
|
||||||
|
"crc32.c",
|
||||||
|
"deflate.c",
|
||||||
|
"gzclose.c",
|
||||||
|
"gzlib.c",
|
||||||
|
"gzread.c",
|
||||||
|
"gzwrite.c",
|
||||||
|
"infback.c",
|
||||||
|
"inffast.c",
|
||||||
|
"inflate.c",
|
||||||
|
"inftrees.c",
|
||||||
|
"trees.c",
|
||||||
|
"uncompr.c",
|
||||||
|
"zutil.c",
|
||||||
|
};
|
36
protocol/eink_feed.proto
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package dergrimm.eink_feed;
|
||||||
|
|
||||||
|
enum Orientation {
|
||||||
|
PORTRAIT_UP = 0;
|
||||||
|
PORTRAIT_DOWN = 1;
|
||||||
|
LANDSCAPE_LEFT = 2;
|
||||||
|
LANDSCAPE_RIGHT = 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
message Display {
|
||||||
|
uint32 width = 1;
|
||||||
|
uint32 height = 2;
|
||||||
|
Orientation orientation = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FrameMessage {
|
||||||
|
Display display = 1;
|
||||||
|
bytes pixels = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ServerMessage {
|
||||||
|
oneof payload { FrameMessage frame = 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeyUpdate {
|
||||||
|
uint32 key_code = 1;
|
||||||
|
string key_name = 2;
|
||||||
|
uint32 value = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ClientKeyUpdateMessage { KeyUpdate key_update = 1; }
|
||||||
|
|
||||||
|
message ClientMessage {
|
||||||
|
oneof payload { ClientKeyUpdateMessage key_update = 1; }
|
||||||
|
}
|
160
src/eink_feed_client/InputListener.zig
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const c = @cImport({
|
||||||
|
@cInclude("errno.h");
|
||||||
|
@cInclude("string.h");
|
||||||
|
@cInclude("libevdev/libevdev.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
const log = std.log.scoped(.input_listener);
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
Evdev,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FdEvdevPair = struct {
|
||||||
|
path: []const u8,
|
||||||
|
fd: std.posix.fd_t,
|
||||||
|
dev: *c.libevdev,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const EventCallback = struct {
|
||||||
|
ptr: *anyopaque,
|
||||||
|
key_event_callback_fn: *const fn (ptr: *anyopaque, device: []const u8, key_code: u32, key_name: []const u8, value: u32) anyerror!void,
|
||||||
|
|
||||||
|
pub fn init(ptr: anytype) EventCallback {
|
||||||
|
const T = @TypeOf(ptr);
|
||||||
|
const ptr_info = @typeInfo(T);
|
||||||
|
if (ptr_info != .pointer) @compileError("ptr must be a pointer");
|
||||||
|
if (ptr_info.pointer.size != .one) @compileError("ptr must be a single item pointer");
|
||||||
|
|
||||||
|
const gen = struct {
|
||||||
|
pub fn keyEventCallback(pointer: *anyopaque, device: []const u8, key_code: u32, key_name: []const u8, value: u32) anyerror!void {
|
||||||
|
const self: T = @ptrCast(@alignCast(pointer));
|
||||||
|
try @call(
|
||||||
|
.always_inline,
|
||||||
|
ptr_info.pointer.child.keyEventCallback,
|
||||||
|
.{ self, device, key_code, key_name, value },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.ptr = ptr,
|
||||||
|
.key_event_callback_fn = gen.keyEventCallback,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keyEventCallback(self: EventCallback, device: []const u8, key_code: u32, key_name: []const u8, value: u32) !void {
|
||||||
|
return self.key_event_callback_fn(self.ptr, device, key_code, key_name, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
devs: []FdEvdevPair,
|
||||||
|
|
||||||
|
event_callback: EventCallback,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, inputs: []const []const u8, event_callback: EventCallback) !Self {
|
||||||
|
const devs = try allocator.alloc(FdEvdevPair, inputs.len);
|
||||||
|
var max_i: usize = 0;
|
||||||
|
errdefer {
|
||||||
|
for (devs[0 .. max_i + 1]) |pair| {
|
||||||
|
c.libevdev_free(pair.dev);
|
||||||
|
std.posix.close(pair.fd);
|
||||||
|
}
|
||||||
|
allocator.free(devs);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (inputs, 0..) |path, i| {
|
||||||
|
const fd = try std.posix.open(path, .{ .ACCMODE = .RDONLY, .NONBLOCK = false }, 0);
|
||||||
|
errdefer std.posix.close(fd);
|
||||||
|
|
||||||
|
var dev: *c.libevdev = undefined;
|
||||||
|
if (c.libevdev_new_from_fd(fd, @ptrCast(&dev)) < 0)
|
||||||
|
return error.Evdev;
|
||||||
|
|
||||||
|
devs[i] = .{
|
||||||
|
.path = try allocator.dupe(u8, path),
|
||||||
|
.fd = fd,
|
||||||
|
.dev = dev,
|
||||||
|
};
|
||||||
|
max_i = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.devs = devs,
|
||||||
|
.event_callback = event_callback,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
for (self.devs) |pair| {
|
||||||
|
self.allocator.free(pair.path);
|
||||||
|
c.libevdev_free(pair.dev);
|
||||||
|
std.posix.close(pair.fd);
|
||||||
|
}
|
||||||
|
self.allocator.free(self.devs);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn printEvent(pair: *const FdEvdevPair, ev: *const c.input_event) void {
|
||||||
|
log.info("Event ({s}): {s} ({}) {s} ({}) => {}", .{
|
||||||
|
pair.path,
|
||||||
|
c.libevdev_event_type_get_name(ev.type),
|
||||||
|
ev.type,
|
||||||
|
c.libevdev_event_code_get_name(ev.type, ev.code),
|
||||||
|
ev.code,
|
||||||
|
ev.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleEvent(self: *const Self, pair: *const FdEvdevPair, ev: *const c.input_event) !void {
|
||||||
|
if (c.libevdev_event_is_type(ev, c.EV_KEY) == 0) return;
|
||||||
|
|
||||||
|
const key_name: []const u8 = std.mem.span(c.libevdev_event_code_get_name(ev.type, ev.code));
|
||||||
|
try self.event_callback.keyEventCallback(pair.path, ev.code, key_name, @intCast(ev.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eventListener(self: *const Self, pair: *const FdEvdevPair) !void {
|
||||||
|
var rc: c_int = c.LIBEVDEV_READ_STATUS_SUCCESS;
|
||||||
|
|
||||||
|
while (rc == c.LIBEVDEV_READ_FLAG_SYNC or rc == c.LIBEVDEV_READ_STATUS_SUCCESS or rc == -c.EAGAIN) {
|
||||||
|
var ev: c.input_event = undefined;
|
||||||
|
rc = c.libevdev_next_event(pair.dev, c.LIBEVDEV_READ_FLAG_NORMAL | c.LIBEVDEV_READ_FLAG_BLOCKING, &ev);
|
||||||
|
switch (rc) {
|
||||||
|
c.LIBEVDEV_READ_STATUS_SYNC => {
|
||||||
|
log.err("{s}: events dropped", .{pair.path});
|
||||||
|
while (rc == c.LIBEVDEV_READ_STATUS_SYNC) {
|
||||||
|
log.err("Sync event ({s})", .{pair.path});
|
||||||
|
printEvent(pair, &ev);
|
||||||
|
try self.handleEvent(pair, &ev);
|
||||||
|
rc = c.libevdev_next_event(pair.dev, c.LIBEVDEV_READ_FLAG_SYNC, &ev);
|
||||||
|
}
|
||||||
|
log.err("{s}: re-synced", .{pair.path});
|
||||||
|
},
|
||||||
|
c.LIBEVDEV_READ_STATUS_SUCCESS => {
|
||||||
|
printEvent(pair, &ev);
|
||||||
|
try self.handleEvent(pair, &ev);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rc != c.LIBEVDEV_READ_STATUS_SYNC and rc != -c.EAGAIN) {
|
||||||
|
log.err("{s}: failed to handle events: {s}", .{ pair.path, c.strerror(-rc) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eventListenerThread(self: *const Self, pair: *const FdEvdevPair) void {
|
||||||
|
self.eventListener(pair) catch unreachable;
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(self: *Self) !void {
|
||||||
|
for (self.devs) |*pair| {
|
||||||
|
_ = try std.Thread.spawn(.{}, eventListenerThread, .{ self, pair });
|
||||||
|
}
|
||||||
|
}
|
103
src/eink_feed_client/Renderer.zig
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const kindle = @import("kindle");
|
||||||
|
const zigimg = @import("zigimg");
|
||||||
|
|
||||||
|
const protocol = @import("eink_feed_protocol");
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
framebuffer: *kindle.eink.fb.Framebuffer,
|
||||||
|
partial: bool,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, framebuffer: *kindle.eink.fb.Framebuffer, partial: bool) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.framebuffer = framebuffer,
|
||||||
|
.partial = partial,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn imageFromFrame(allocator: std.mem.Allocator, frame: *const protocol.models.Frame) !zigimg.Image {
|
||||||
|
return zigimg.Image.fromRawPixels(
|
||||||
|
allocator,
|
||||||
|
@as(usize, frame.display.width),
|
||||||
|
@as(usize, frame.display.height),
|
||||||
|
frame.pixels,
|
||||||
|
.grayscale8,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixImageForFb(self: *const Self, display: *const protocol.models.Display, image: *zigimg.Image) !zigimg.Image {
|
||||||
|
try image.convert(.grayscale4);
|
||||||
|
|
||||||
|
var fixed = try zigimg.Image.create(
|
||||||
|
self.allocator,
|
||||||
|
self.framebuffer.xres,
|
||||||
|
self.framebuffer.yres,
|
||||||
|
.grayscale4,
|
||||||
|
);
|
||||||
|
errdefer fixed.deinit();
|
||||||
|
@memset(fixed.pixels.grayscale4, zigimg.color.Grayscale4{ .value = std.math.maxInt(u4) });
|
||||||
|
|
||||||
|
switch (display.orientation) {
|
||||||
|
.portrait_up => {
|
||||||
|
const width = if (image.width > fixed.width) fixed.width else image.width;
|
||||||
|
const height = if (image.height > fixed.height) fixed.height else image.height;
|
||||||
|
for (0..height) |row| {
|
||||||
|
for (0..width) |column| {
|
||||||
|
const idx = row * fixed.width + column;
|
||||||
|
fixed.pixels.grayscale4[idx].value = image.pixels.grayscale4[row * image.width + column].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.portrait_down => {
|
||||||
|
const width = if (image.width > fixed.width) fixed.width else image.width;
|
||||||
|
const height = if (image.height > fixed.height) fixed.height else image.height;
|
||||||
|
for (0..height) |row| {
|
||||||
|
for (0..width) |column| {
|
||||||
|
const idx = ((fixed.height - 1 - row) * fixed.width) + (fixed.width - 1 - column);
|
||||||
|
fixed.pixels.grayscale4[idx].value = image.pixels.grayscale4[row * image.width + column].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.landscape_left => {
|
||||||
|
const width = if (image.width > fixed.height) fixed.height else image.width;
|
||||||
|
const height = if (image.height > fixed.width) fixed.width else image.height;
|
||||||
|
for (0..height) |row| {
|
||||||
|
for (0..width) |column| {
|
||||||
|
const idx = ((fixed.height - 1 - column) * fixed.width) + row;
|
||||||
|
fixed.pixels.grayscale4[idx].value = image.pixels.grayscale4[row * image.width + column].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.landscape_right => {
|
||||||
|
const width = if (image.width > fixed.height) fixed.height else image.width;
|
||||||
|
const height = if (image.height > fixed.width) fixed.width else image.height;
|
||||||
|
for (0..height) |row| {
|
||||||
|
for (0..width) |column| {
|
||||||
|
const idx = ((column + 1) * fixed.width) - 1 - row;
|
||||||
|
fixed.pixels.grayscale4[idx].value = image.pixels.grayscale4[row * image.width + column].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(self: *Self, image: *const zigimg.Image) !void {
|
||||||
|
if (image.width != self.framebuffer.xres or image.height != self.framebuffer.yres)
|
||||||
|
return;
|
||||||
|
|
||||||
|
self.framebuffer.fill(0x00);
|
||||||
|
|
||||||
|
for (0..image.height) |row| {
|
||||||
|
for (0..image.width) |column| {
|
||||||
|
const color: u8 = 0xff - 16 * @as(u8, image.pixels.grayscale4[row * image.width + column].value);
|
||||||
|
self.framebuffer.set(column, row, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.framebuffer.update(if (self.partial) .partial else .full);
|
||||||
|
}
|
169
src/eink_feed_client/WebsocketClient.zig
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const c = @cImport({
|
||||||
|
@cInclude("curl/curl.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
const log = std.log.scoped(.ws_client);
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub const ConnectOptions = struct {
|
||||||
|
url: []const u8,
|
||||||
|
wss: bool = false,
|
||||||
|
verbose: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WriterCallback = struct {
|
||||||
|
pub const Meta = c.curl_ws_frame;
|
||||||
|
|
||||||
|
ptr: *anyopaque,
|
||||||
|
write_callback_fn: *const fn (ptr: *anyopaque, data: []const u8, meta: *const Meta) anyerror!void,
|
||||||
|
|
||||||
|
pub fn init(ptr: anytype) WriterCallback {
|
||||||
|
const T = @TypeOf(ptr);
|
||||||
|
const ptr_info = @typeInfo(T);
|
||||||
|
if (ptr_info != .pointer) @compileError("ptr must be a pointer");
|
||||||
|
if (ptr_info.pointer.size != .one) @compileError("ptr must be a single item pointer");
|
||||||
|
|
||||||
|
const gen = struct {
|
||||||
|
pub fn writeCallback(pointer: *anyopaque, data: []const u8, meta: *const Meta) anyerror!void {
|
||||||
|
const self: T = @ptrCast(@alignCast(pointer));
|
||||||
|
try @call(
|
||||||
|
.always_inline,
|
||||||
|
ptr_info.pointer.child.writeCallback,
|
||||||
|
.{ self, data, meta },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.ptr = ptr,
|
||||||
|
.write_callback_fn = gen.writeCallback,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeCallback(self: WriterCallback, data: []const u8, meta: *const Meta) !void {
|
||||||
|
return self.write_callback_fn(self.ptr, data, meta);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
connect_options: ConnectOptions,
|
||||||
|
callback_writer: WriterCallback,
|
||||||
|
curl: ?*c.CURL = null,
|
||||||
|
send_lock: std.Thread.Mutex = .{},
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
connect_options: ConnectOptions,
|
||||||
|
callback_writer: WriterCallback,
|
||||||
|
) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.connect_options = connect_options,
|
||||||
|
.callback_writer = callback_writer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CurlError = error{
|
||||||
|
curl,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn handleCurlCode(result: c.CURLcode) CurlError!void {
|
||||||
|
if (result != c.CURLE_OK) {
|
||||||
|
log.err("curl_easy_perform() failed: {s} ({d})", .{ c.curl_easy_strerror(result), result });
|
||||||
|
return CurlError.curl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CallbackData = struct {
|
||||||
|
curl: *c.CURL,
|
||||||
|
writer: *const WriterCallback,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn writeCallback(data: [*c]u8, size: usize, nitems: usize, clientp: *anyopaque) callconv(.C) c_ulong {
|
||||||
|
const cb_data = @as(*CallbackData, @ptrCast(@alignCast(clientp)));
|
||||||
|
|
||||||
|
const realsize = nitems * size;
|
||||||
|
const buf = data[0..realsize];
|
||||||
|
|
||||||
|
const meta = c.curl_ws_meta(cb_data.curl).?;
|
||||||
|
cb_data.writer.writeCallback(buf, meta) catch |err| {
|
||||||
|
log.err("unrecoverable error during protobuf decoding: {}", .{err});
|
||||||
|
unreachable;
|
||||||
|
};
|
||||||
|
|
||||||
|
return realsize;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sendMessage(self: *Self, data: []const u8) !void {
|
||||||
|
if (self.curl) |curl| {
|
||||||
|
self.send_lock.lock();
|
||||||
|
defer self.send_lock.unlock();
|
||||||
|
|
||||||
|
var sent: usize = undefined;
|
||||||
|
try handleCurlCode(
|
||||||
|
c.curl_ws_send(curl, @ptrCast(data), data.len, &sent, 0, c.CURLWS_TEXT),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.close();
|
||||||
|
if (self.curl) |curl| {
|
||||||
|
c.curl_easy_cleanup(curl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(self: *Self) void {
|
||||||
|
if (self.curl) |curl| {
|
||||||
|
var sent: usize = undefined;
|
||||||
|
_ = c.curl_ws_send(curl, &{}, 0, &sent, 0, c.CURLWS_CLOSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect(self: *Self, client_name: []const u8) !void {
|
||||||
|
self.close();
|
||||||
|
|
||||||
|
const curl: *c.CURL = c.curl_easy_init().?;
|
||||||
|
self.curl = curl;
|
||||||
|
|
||||||
|
const escaped_client_name = blk: {
|
||||||
|
const s: ?[*:0]u8 = c.curl_easy_escape(curl, @ptrCast(client_name), @intCast(client_name.len));
|
||||||
|
if (s == null) {
|
||||||
|
return CurlError.curl;
|
||||||
|
}
|
||||||
|
break :blk s.?;
|
||||||
|
};
|
||||||
|
defer c.curl_free(escaped_client_name);
|
||||||
|
|
||||||
|
const url = try std.fmt.allocPrint(self.allocator, "{s}://{s}?name={s}", .{
|
||||||
|
if (self.connect_options.wss) "wss" else "ws",
|
||||||
|
self.connect_options.url,
|
||||||
|
escaped_client_name,
|
||||||
|
});
|
||||||
|
defer self.allocator.free(url);
|
||||||
|
|
||||||
|
try handleCurlCode(
|
||||||
|
c.curl_easy_setopt(curl, c.CURLOPT_URL, @as([*:0]u8, @ptrCast(url))),
|
||||||
|
);
|
||||||
|
if (self.connect_options.verbose) {
|
||||||
|
try handleCurlCode(
|
||||||
|
c.curl_easy_setopt(curl, c.CURLOPT_VERBOSE, @as(c_long, 1)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cb_data = CallbackData{
|
||||||
|
.curl = curl,
|
||||||
|
.writer = &self.callback_writer,
|
||||||
|
};
|
||||||
|
try handleCurlCode(
|
||||||
|
c.curl_easy_setopt(curl, c.CURLOPT_WRITEFUNCTION, writeCallback),
|
||||||
|
);
|
||||||
|
try handleCurlCode(c.curl_easy_setopt(curl, c.CURLOPT_WRITEDATA, &cb_data));
|
||||||
|
|
||||||
|
try handleCurlCode(
|
||||||
|
c.curl_easy_perform(curl),
|
||||||
|
);
|
||||||
|
}
|
190
src/eink_feed_client/main.zig
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const kindle = @import("kindle");
|
||||||
|
const clap = @import("clap");
|
||||||
|
|
||||||
|
const protocol = @import("eink_feed_protocol");
|
||||||
|
|
||||||
|
const client = @import("root.zig");
|
||||||
|
|
||||||
|
pub const std_options: std.Options = .{
|
||||||
|
.log_level = if (builtin.mode == .Debug) .debug else .info,
|
||||||
|
};
|
||||||
|
|
||||||
|
const log = std.log.scoped(.eink_feed_client);
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{
|
||||||
|
.thread_safe = true,
|
||||||
|
}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
{
|
||||||
|
const params = comptime clap.parseParamsComptime(
|
||||||
|
\\-h, --help Display this help and exit.
|
||||||
|
\\--host <str> eink-feed server host (e.g. hostname:port).
|
||||||
|
\\--name <str> Client name.
|
||||||
|
\\--partial Enable partial eink framebuffer updates. May not look as sharp but reduces flickering.
|
||||||
|
);
|
||||||
|
|
||||||
|
var diag = clap.Diagnostic{};
|
||||||
|
var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{
|
||||||
|
.diagnostic = &diag,
|
||||||
|
.allocator = allocator,
|
||||||
|
}) catch |err| {
|
||||||
|
diag.report(std.io.getStdErr().writer(), err) catch {};
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
defer res.deinit();
|
||||||
|
|
||||||
|
if (res.args.help != 0 or
|
||||||
|
res.args.host == null or
|
||||||
|
res.args.name == null)
|
||||||
|
{
|
||||||
|
const writer = std.io.getStdErr().writer();
|
||||||
|
try writer.writeAll("eink-feed-client\n");
|
||||||
|
return clap.help(writer, clap.Help, ¶ms, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
const client_name: []const u8 = res.args.name.?;
|
||||||
|
const ws_url = try std.fmt.allocPrint(allocator, "{s}/api/ws/display", .{res.args.host.?});
|
||||||
|
defer allocator.free(ws_url);
|
||||||
|
|
||||||
|
var fb = try kindle.eink.fb.Framebuffer.open("/dev/fb0");
|
||||||
|
defer fb.deinit();
|
||||||
|
try fb.clear();
|
||||||
|
|
||||||
|
var renderer = client.Renderer.init(allocator, &fb, res.args.partial != 0);
|
||||||
|
|
||||||
|
var cb_writer = FeedCallbackWriter.init(allocator, &renderer);
|
||||||
|
defer cb_writer.deinit();
|
||||||
|
var ws = client.WebsocketClient.init(
|
||||||
|
allocator,
|
||||||
|
.{ .url = ws_url },
|
||||||
|
cb_writer.writer(),
|
||||||
|
);
|
||||||
|
defer ws.deinit();
|
||||||
|
|
||||||
|
var event_callback = FeedEventCallback.init(allocator, &ws);
|
||||||
|
var input_listener = try client.InputListener.init(
|
||||||
|
allocator,
|
||||||
|
&.{ "/dev/input/event0", "/dev/input/event1" },
|
||||||
|
event_callback.listener(),
|
||||||
|
);
|
||||||
|
defer input_listener.deinit();
|
||||||
|
try input_listener.start();
|
||||||
|
|
||||||
|
try ws.connect(client_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeedEventCallback = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
ws: *client.WebsocketClient,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, ws: *client.WebsocketClient) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.ws = ws,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listener(self: *Self) client.InputListener.EventCallback {
|
||||||
|
return client.InputListener.EventCallback.init(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keyEventCallback(self: *const Self, device: []const u8, key_code: u32, key_name: []const u8, value: u32) !void {
|
||||||
|
_ = device;
|
||||||
|
|
||||||
|
if (!std.mem.startsWith(u8, key_name, "KEY_")) return;
|
||||||
|
const trim_name = key_name[4..];
|
||||||
|
|
||||||
|
const key_update = protocol.protobuf.KeyUpdate{
|
||||||
|
.key_code = key_code,
|
||||||
|
.key_name = .{ .Const = trim_name },
|
||||||
|
.value = value,
|
||||||
|
};
|
||||||
|
const msg = protocol.protobuf.ClientMessage{
|
||||||
|
.key_update = .{ .key_update = key_update },
|
||||||
|
};
|
||||||
|
|
||||||
|
const buf = try msg.encode(self.allocator);
|
||||||
|
defer self.allocator.free(buf);
|
||||||
|
|
||||||
|
try self.ws.sendMessage(buf);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const FeedCallbackWriter = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
data: ?[]u8,
|
||||||
|
|
||||||
|
renderer: *client.Renderer,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, renderer: *client.Renderer) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.data = null,
|
||||||
|
.renderer = renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Self) void {
|
||||||
|
if (self.data) |data| self.allocator.free(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleMsg(self: *const Self, buf: []const u8) !void {
|
||||||
|
const msg = try protocol.protobuf.ServerMessage.decode(buf, self.allocator);
|
||||||
|
defer msg.deinit();
|
||||||
|
|
||||||
|
if (msg.frame) |frame_pb| {
|
||||||
|
const frame = protocol.models.Frame{
|
||||||
|
.display = protocol.models.Display.fromProtobuf(frame_pb.display.?),
|
||||||
|
.pixels = frame_pb.pixels.getSlice(),
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Received frame update: w = {d}, h = {d}, orientation = {s}",
|
||||||
|
.{ frame.display.width, frame.display.height, frame.display.orientation.toString() },
|
||||||
|
);
|
||||||
|
|
||||||
|
var image = try client.Renderer.imageFromFrame(self.allocator, &frame);
|
||||||
|
defer image.deinit();
|
||||||
|
var fixed_image = try self.renderer.fixImageForFb(&frame.display, &image);
|
||||||
|
defer fixed_image.deinit();
|
||||||
|
try self.renderer.render(&fixed_image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeCallback(self: *Self, buf: []const u8, meta: *const client.WebsocketClient.WriterCallback.Meta) !void {
|
||||||
|
if (self.data) |data| {
|
||||||
|
if (meta.bytesleft == 0) {
|
||||||
|
defer {
|
||||||
|
self.allocator.free(data);
|
||||||
|
self.data = null;
|
||||||
|
}
|
||||||
|
const offset: usize = @intCast(meta.offset);
|
||||||
|
@memcpy(data[offset .. offset + buf.len], buf);
|
||||||
|
try self.handleMsg(data);
|
||||||
|
} else {
|
||||||
|
const offset: usize = @intCast(meta.offset);
|
||||||
|
@memcpy(data[offset .. offset + buf.len], buf);
|
||||||
|
}
|
||||||
|
} else if (meta.bytesleft == 0) {
|
||||||
|
try self.handleMsg(buf);
|
||||||
|
} else {
|
||||||
|
const size: usize = buf.len + @as(usize, @intCast(meta.bytesleft));
|
||||||
|
self.data = try self.allocator.alloc(u8, size);
|
||||||
|
@memcpy(self.data.?[0..buf.len], buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writer(self: *Self) client.WebsocketClient.WriterCallback {
|
||||||
|
return client.WebsocketClient.WriterCallback.init(self);
|
||||||
|
}
|
||||||
|
};
|
3
src/eink_feed_client/root.zig
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub const WebsocketClient = @import("WebsocketClient.zig");
|
||||||
|
pub const Renderer = @import("Renderer.zig");
|
||||||
|
pub const InputListener = @import("InputListener.zig");
|
119
src/eink_feed_protocol/models.zig
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const protobuf = @import("root.zig").protobuf;
|
||||||
|
|
||||||
|
// pub const PixelFormat = enum {
|
||||||
|
// grayscale,
|
||||||
|
|
||||||
|
// pub fn fromProtobuf(value: protobuf.PixelFormat) PixelFormat {
|
||||||
|
// return switch (value) {
|
||||||
|
// .GRAYSCALE => .grayscale,
|
||||||
|
// _ => .grayscale,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn toProtobuf(self: PixelFormat) protobuf.PixelFormat {
|
||||||
|
// return switch (self) {
|
||||||
|
// .grayscale => .GRAYSCALE,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
pub const Orientation = enum {
|
||||||
|
portrait_up,
|
||||||
|
portrait_down,
|
||||||
|
landscape_left,
|
||||||
|
landscape_right,
|
||||||
|
|
||||||
|
pub fn fromString(str: []const u8) ?Orientation {
|
||||||
|
if (std.mem.eql(u8, str, "portrait_up")) {
|
||||||
|
return .portrait_up;
|
||||||
|
} else if (std.mem.eql(u8, str, "portrait_down")) {
|
||||||
|
return .portrait_down;
|
||||||
|
} else if (std.mem.eql(u8, str, "landscape_left")) {
|
||||||
|
return .landscape_left;
|
||||||
|
} else if (std.mem.eql(u8, str, "landscape_right")) {
|
||||||
|
return .landscape_right;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: Orientation) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.portrait_up => "portrait_up",
|
||||||
|
.portrait_down => "portrait_down",
|
||||||
|
.landscape_left => "landscape_left",
|
||||||
|
.landscape_right => "landscape_right",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fromProtobuf(value: protobuf.Orientation) Orientation {
|
||||||
|
return switch (value) {
|
||||||
|
.PORTRAIT_UP => .portrait_up,
|
||||||
|
.PORTRAIT_DOWN => .portrait_down,
|
||||||
|
.LANDSCAPE_LEFT => .landscape_left,
|
||||||
|
.LANDSCAPE_RIGHT => .landscape_right,
|
||||||
|
_ => .portrait_up,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toProtobuf(self: Orientation) protobuf.Orientation {
|
||||||
|
return switch (self) {
|
||||||
|
.portrait_up => .PORTRAIT_UP,
|
||||||
|
.portrait_down => .PORTRAIT_DOWN,
|
||||||
|
.landscape_left => .LANDSCAPE_LEFT,
|
||||||
|
.landscape_right => .LANDSCAPE_RIGHT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Display = struct {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
orientation: Orientation,
|
||||||
|
|
||||||
|
pub fn fromProtobuf(value: protobuf.Display) Display {
|
||||||
|
return .{
|
||||||
|
// .format = PixelFormat.fromProtobuf(value.format),
|
||||||
|
.width = value.width,
|
||||||
|
.height = value.height,
|
||||||
|
.orientation = Orientation.fromProtobuf(value.orientation),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toProtobuf(self: Display, allocator: std.mem.Allocator) protobuf.Display {
|
||||||
|
var display = protobuf.Display.init(allocator);
|
||||||
|
// display.format = self.format.toProtobuf();
|
||||||
|
display.width = self.width;
|
||||||
|
display.height = self.height;
|
||||||
|
display.orientation = self.orientation.toProtobuf();
|
||||||
|
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// pub const FrameConfig = struct {
|
||||||
|
// fps: ?u32 = null,
|
||||||
|
// display: Display,
|
||||||
|
|
||||||
|
// pub fn fromProtobuf(value: protobuf.FrameConfig) FrameConfig {
|
||||||
|
// return .{
|
||||||
|
// .fps = value.fps,
|
||||||
|
// .display = Display.fromProtobuf(value.display.?),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn toProtobuf(self: FrameConfig, allocator: std.mem.Allocator) protobuf.FrameConfig {
|
||||||
|
// var frame_config = protobuf.FrameConfig.init(allocator);
|
||||||
|
// frame_config.fps = self.fps;
|
||||||
|
// frame_config.display = self.display.toProtobuf(allocator);
|
||||||
|
|
||||||
|
// return frame_config;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
pub const Frame = struct {
|
||||||
|
display: Display,
|
||||||
|
pixels: []const u8,
|
||||||
|
};
|
88
src/eink_feed_protocol/proto/dergrimm/eink_feed.pb.zig
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// Code generated by protoc-gen-zig
|
||||||
|
///! package dergrimm.eink_feed
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ArrayList = std.ArrayList;
|
||||||
|
|
||||||
|
const protobuf = @import("protobuf");
|
||||||
|
const ManagedString = protobuf.ManagedString;
|
||||||
|
const fd = protobuf.fd;
|
||||||
|
const ManagedStruct = protobuf.ManagedStruct;
|
||||||
|
|
||||||
|
pub const Orientation = enum(i32) {
|
||||||
|
PORTRAIT_UP = 0,
|
||||||
|
PORTRAIT_DOWN = 1,
|
||||||
|
LANDSCAPE_LEFT = 2,
|
||||||
|
LANDSCAPE_RIGHT = 3,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Display = struct {
|
||||||
|
width: u32 = 0,
|
||||||
|
height: u32 = 0,
|
||||||
|
orientation: Orientation = @enumFromInt(0),
|
||||||
|
|
||||||
|
pub const _desc_table = .{
|
||||||
|
.width = fd(1, .{ .Varint = .Simple }),
|
||||||
|
.height = fd(2, .{ .Varint = .Simple }),
|
||||||
|
.orientation = fd(3, .{ .Varint = .Simple }),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub usingnamespace protobuf.MessageMixins(@This());
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FrameMessage = struct {
|
||||||
|
display: ?Display = null,
|
||||||
|
pixels: ManagedString = .Empty,
|
||||||
|
|
||||||
|
pub const _desc_table = .{
|
||||||
|
.display = fd(1, .{ .SubMessage = {} }),
|
||||||
|
.pixels = fd(2, .Bytes),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub usingnamespace protobuf.MessageMixins(@This());
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ServerMessage = struct {
|
||||||
|
frame: ?FrameMessage = null,
|
||||||
|
|
||||||
|
pub const _desc_table = .{
|
||||||
|
.frame = fd(1, .{ .SubMessage = {} }),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub usingnamespace protobuf.MessageMixins(@This());
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const KeyUpdate = struct {
|
||||||
|
key_code: u32 = 0,
|
||||||
|
key_name: ManagedString = .Empty,
|
||||||
|
value: u32 = 0,
|
||||||
|
|
||||||
|
pub const _desc_table = .{
|
||||||
|
.key_code = fd(1, .{ .Varint = .Simple }),
|
||||||
|
.key_name = fd(2, .String),
|
||||||
|
.value = fd(3, .{ .Varint = .Simple }),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub usingnamespace protobuf.MessageMixins(@This());
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ClientKeyUpdateMessage = struct {
|
||||||
|
key_update: ?KeyUpdate = null,
|
||||||
|
|
||||||
|
pub const _desc_table = .{
|
||||||
|
.key_update = fd(1, .{ .SubMessage = {} }),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub usingnamespace protobuf.MessageMixins(@This());
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ClientMessage = struct {
|
||||||
|
key_update: ?ClientKeyUpdateMessage = null,
|
||||||
|
|
||||||
|
pub const _desc_table = .{
|
||||||
|
.key_update = fd(1, .{ .SubMessage = {} }),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub usingnamespace protobuf.MessageMixins(@This());
|
||||||
|
};
|
2
src/eink_feed_protocol/root.zig
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub const protobuf = @import("proto/dergrimm/eink_feed.pb.zig");
|
||||||
|
pub const models = @import("models.zig");
|
2
src/eink_feed_render/Dimensions.zig
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
110
src/eink_feed_render/FeedClient.zig
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const wardrobe = @import("wardrobe");
|
||||||
|
|
||||||
|
const server = @import("eink_feed_server");
|
||||||
|
const protocol = @import("eink_feed_protocol");
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
InvalidResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
channel_url_str: []const u8,
|
||||||
|
frame_url_str: []const u8,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, base_url: []const u8) !Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.channel_url_str = try std.fmt.allocPrint(allocator, "{s}/api/channel", .{base_url}),
|
||||||
|
.frame_url_str = try std.fmt.allocPrint(allocator, "{s}/api/frame", .{base_url}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *const Self) void {
|
||||||
|
self.allocator.free(self.channel_url_str);
|
||||||
|
self.allocator.free(self.frame_url_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getChannel(self: *const Self, channel_id: server.models.Channel.Id) !std.json.Parsed(server.web.api.models.Channel) {
|
||||||
|
var client = std.http.Client{ .allocator = self.allocator };
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
const url_str = try std.fmt.allocPrint(
|
||||||
|
self.allocator,
|
||||||
|
"{s}?id={d}",
|
||||||
|
.{ self.channel_url_str, channel_id },
|
||||||
|
);
|
||||||
|
defer self.allocator.free(url_str);
|
||||||
|
const url = try std.Uri.parse(url_str);
|
||||||
|
|
||||||
|
var buf: [4096]u8 = undefined;
|
||||||
|
var req = try client.open(.GET, url, .{
|
||||||
|
.server_header_buffer = &buf,
|
||||||
|
});
|
||||||
|
defer req.deinit();
|
||||||
|
|
||||||
|
try req.send();
|
||||||
|
try req.finish();
|
||||||
|
try req.wait();
|
||||||
|
|
||||||
|
if (req.response.status != .ok)
|
||||||
|
return Error.InvalidResponse;
|
||||||
|
|
||||||
|
const body = try req.reader().readAllAlloc(self.allocator, 1 * 1024 * 1024);
|
||||||
|
defer self.allocator.free(body);
|
||||||
|
|
||||||
|
const channel = try std.json.parseFromSlice(
|
||||||
|
server.web.api.models.Channel,
|
||||||
|
self.allocator,
|
||||||
|
body,
|
||||||
|
.{ .allocate = .alloc_always, .ignore_unknown_fields = true },
|
||||||
|
);
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uploadFrame(self: *const Self, channel_id: u32, orientation: protocol.models.Orientation, data: []const u8, content_type: []const u8) !void {
|
||||||
|
var client = std.http.Client{ .allocator = self.allocator };
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
const url_str = try std.fmt.allocPrint(
|
||||||
|
self.allocator,
|
||||||
|
"{s}?id={d}&orientation={s}",
|
||||||
|
.{ self.frame_url_str, channel_id, orientation.toString() },
|
||||||
|
);
|
||||||
|
defer self.allocator.free(url_str);
|
||||||
|
const url = try std.Uri.parse(url_str);
|
||||||
|
|
||||||
|
var body = std.ArrayList(u8).init(self.allocator);
|
||||||
|
defer body.deinit();
|
||||||
|
|
||||||
|
var prng = std.Random.DefaultPrng.init(@intCast(std.time.microTimestamp()));
|
||||||
|
const boundary: wardrobe.Boundary = .entropy("EinkFeedRenderBoundary", prng.random());
|
||||||
|
var write_stream = wardrobe.writeStream(boundary, body.writer());
|
||||||
|
|
||||||
|
try write_stream.beginFileEntry("img", content_type, "image.png");
|
||||||
|
try write_stream.writer().writeAll(data);
|
||||||
|
try write_stream.endEntry();
|
||||||
|
|
||||||
|
try write_stream.endEntries();
|
||||||
|
|
||||||
|
var buf: [4096]u8 = undefined;
|
||||||
|
var req = try client.open(.POST, url, .{
|
||||||
|
.server_header_buffer = &buf,
|
||||||
|
.headers = .{
|
||||||
|
.content_type = .{ .override = boundary.contentType() },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
defer req.deinit();
|
||||||
|
req.transfer_encoding = .{ .content_length = body.items.len };
|
||||||
|
|
||||||
|
try req.send();
|
||||||
|
try req.writeAll(body.items);
|
||||||
|
try req.finish();
|
||||||
|
try req.wait();
|
||||||
|
|
||||||
|
if (req.response.status != .created)
|
||||||
|
return Error.InvalidResponse;
|
||||||
|
}
|
219
src/eink_feed_render/apps/Departures/efa.zig
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const escaper = @import("../../escaper.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.efa);
|
||||||
|
|
||||||
|
pub const ApiTransportationProductClass = enum(u32) {
|
||||||
|
zug = 0,
|
||||||
|
s_bahn = 1,
|
||||||
|
u_bahn = 2,
|
||||||
|
stadtbahn = 3,
|
||||||
|
strassen_trambahn = 4,
|
||||||
|
stadtbus = 5,
|
||||||
|
regionalbus = 6,
|
||||||
|
schnellbus = 7,
|
||||||
|
seil_zahnradbahn = 8,
|
||||||
|
schiff = 9,
|
||||||
|
anruf_sammel_taxi = 10,
|
||||||
|
sonstige = 11,
|
||||||
|
flugzeug = 12,
|
||||||
|
zug_nv = 13,
|
||||||
|
zug_fv = 14,
|
||||||
|
zug_fv_m_zuschlag = 15,
|
||||||
|
zug_fv_m_spez_fpr = 16,
|
||||||
|
sev = 17,
|
||||||
|
zug_shuttle = 18,
|
||||||
|
buergerbus = 19,
|
||||||
|
rufbus_liniengebunden = 20,
|
||||||
|
rufbus = 21,
|
||||||
|
_,
|
||||||
|
|
||||||
|
pub fn toString(self: ApiTransportationProductClass) ?[]const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.zug => "Zug",
|
||||||
|
.s_bahn => "S-Bahn",
|
||||||
|
.u_bahn => "U-Bahn",
|
||||||
|
.stadtbahn => "Stadtbahn",
|
||||||
|
.strassen_trambahn => "Straßen-/Trambahn",
|
||||||
|
.stadtbus => "Stadtbus",
|
||||||
|
.regionalbus => "Regionalbus",
|
||||||
|
.schnellbus => "Schnellbus",
|
||||||
|
.seil_zahnradbahn => "Seil-/Zahnradbahn",
|
||||||
|
.schiff => "Schiff",
|
||||||
|
.anruf_sammel_taxi => "Anruf-Sammel-Taxi",
|
||||||
|
.sonstige => "Sonstige",
|
||||||
|
.flugzeug => "Flugzeug",
|
||||||
|
.zug_nv => "Zug (Nahverkehr)",
|
||||||
|
.zug_fv => "Zug (Fernverkehr)",
|
||||||
|
.zug_fv_m_zuschlag => "Zug (Fernverkehr) mit Zuschlag",
|
||||||
|
.zug_fv_m_spez_fpr => "Zug (Fernverkehr) mit spezifischer Fpr",
|
||||||
|
.sev => "SEV Schnienenersatzverkehr",
|
||||||
|
.zug_shuttle => "Zug Shuttle",
|
||||||
|
.buergerbus => "Bürgerbus",
|
||||||
|
.rufbus_liniengebunden => "Rufbus (liniengebunden)",
|
||||||
|
.rufbus => "Rufbus",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ApiLocation = struct {
|
||||||
|
id: []const u8,
|
||||||
|
name: []const u8,
|
||||||
|
properties: struct {
|
||||||
|
platform: ?[]const u8 = null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ApiStopEvent = struct {
|
||||||
|
location: ApiLocation,
|
||||||
|
departureTimePlanned: []const u8,
|
||||||
|
departureTimeBaseTimetable: []const u8,
|
||||||
|
departureTimeEstimated: ?[]const u8 = null,
|
||||||
|
transportation: struct {
|
||||||
|
name: []const u8,
|
||||||
|
product: struct {
|
||||||
|
class: ApiTransportationProductClass,
|
||||||
|
},
|
||||||
|
operator: ?struct {
|
||||||
|
name: []const u8,
|
||||||
|
} = null,
|
||||||
|
destination: struct {
|
||||||
|
name: []const u8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hints: ?[]const struct {
|
||||||
|
content: []const u8,
|
||||||
|
type: []const u8,
|
||||||
|
} = null,
|
||||||
|
isCancelled: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DmResponse = struct {
|
||||||
|
locations: []const ApiLocation,
|
||||||
|
stopEvents: []const ApiStopEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Client = struct {
|
||||||
|
pub const Error = error{
|
||||||
|
InvalidResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
const BodyLimit: usize = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
efa_url: []const u8,
|
||||||
|
efa_dm_url_str: []const u8,
|
||||||
|
efa_stopfinder_url_str: []const u8,
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
efa_url: []const u8,
|
||||||
|
) !Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.efa_url = efa_url,
|
||||||
|
.efa_dm_url_str = try std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
"{s}/XML_DM_REQUEST?locationServerActive=1&stateless=1&sRaLP=1&itdLPxx_generalInfo=false&mode=direct&type_dm=any&itdLPxx_stopname=false&useRealtime=1&deleteAssignedStops_dm=1&depType=stopEvents&useAllStops=1&outputFormat=rapidJSON",
|
||||||
|
.{efa_url},
|
||||||
|
),
|
||||||
|
.efa_stopfinder_url_str = try std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
"{s}/XML_STOPFINDER_REQUEST?doNotSearchForStops_sf=1&locationInfoActive=0&locationServerActive=0&sl3plusStopFinderMacro=1&type_sf=any&outputFormat=rapidJSON",
|
||||||
|
.{efa_url},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *const Self) void {
|
||||||
|
self.allocator.free(self.efa_stopfinder_url_str);
|
||||||
|
self.allocator.free(self.efa_dm_url_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn findStops(self: *const Self, query: []const u8) !void {
|
||||||
|
const escaped_query = try escaper.escapeUriComponent(self.allocator, query);
|
||||||
|
defer if (escaped_query) |s| self.allocator.free(s);
|
||||||
|
const url_query = escaped_query orelse query;
|
||||||
|
|
||||||
|
var client = std.http.Client{ .allocator = self.allocator };
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
const url_str = try std.fmt.allocPrint(
|
||||||
|
self.allocator,
|
||||||
|
"{s}&name_sf={s}",
|
||||||
|
.{ self.efa_stopfinder_url_str, url_query },
|
||||||
|
);
|
||||||
|
defer self.allocator.free(url_str);
|
||||||
|
const url = try std.Uri.parse(url_str);
|
||||||
|
log.info("Finding stops ({s}): {s}", .{ query, url_str });
|
||||||
|
|
||||||
|
var buf: [4096]u8 = undefined;
|
||||||
|
var req = try client.open(.GET, url, .{
|
||||||
|
.server_header_buffer = &buf,
|
||||||
|
});
|
||||||
|
defer req.deinit();
|
||||||
|
|
||||||
|
try req.send();
|
||||||
|
try req.finish();
|
||||||
|
try req.wait();
|
||||||
|
|
||||||
|
if (req.response.status != .ok)
|
||||||
|
return Error.InvalidResponse;
|
||||||
|
|
||||||
|
const body = try req.reader().readAllAlloc(self.allocator, BodyLimit);
|
||||||
|
defer self.allocator.free(body);
|
||||||
|
|
||||||
|
try std.io.getStdOut().writeAll(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getDepartures(self: *const Self, stop_id: []const u8) !std.json.Parsed(DmResponse) {
|
||||||
|
const escaped_stop_id = try escaper.escapeUriComponent(self.allocator, stop_id);
|
||||||
|
defer if (escaped_stop_id) |s| self.allocator.free(s);
|
||||||
|
const url_stop_id = escaped_stop_id orelse stop_id;
|
||||||
|
|
||||||
|
var client = std.http.Client{ .allocator = self.allocator };
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
const url_str = try std.fmt.allocPrint(
|
||||||
|
self.allocator,
|
||||||
|
"{s}&name_dm={s}",
|
||||||
|
.{ self.efa_dm_url_str, url_stop_id },
|
||||||
|
);
|
||||||
|
defer self.allocator.free(url_str);
|
||||||
|
const url = try std.Uri.parse(url_str);
|
||||||
|
log.info("Fetching departures: {s}", .{url_str});
|
||||||
|
|
||||||
|
var buf: [4096]u8 = undefined;
|
||||||
|
var req = try client.open(.GET, url, .{
|
||||||
|
.server_header_buffer = &buf,
|
||||||
|
});
|
||||||
|
defer req.deinit();
|
||||||
|
|
||||||
|
try req.send();
|
||||||
|
try req.finish();
|
||||||
|
try req.wait();
|
||||||
|
|
||||||
|
if (req.response.status != .ok)
|
||||||
|
return Error.InvalidResponse;
|
||||||
|
|
||||||
|
const body = try req.reader().readAllAlloc(self.allocator, BodyLimit);
|
||||||
|
defer self.allocator.free(body);
|
||||||
|
|
||||||
|
const response = try std.json.parseFromSlice(
|
||||||
|
DmResponse,
|
||||||
|
self.allocator,
|
||||||
|
body,
|
||||||
|
.{ .allocate = .alloc_always, .ignore_unknown_fields = true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// const str = try std.json.stringifyAlloc(self.allocator, response.value, .{});
|
||||||
|
// defer self.allocator.free(str);
|
||||||
|
// try std.io.getStdOut().writeAll(str);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
131
src/eink_feed_render/apps/Departures/root.zig
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const temp = @import("temp");
|
||||||
|
const zdt = @import("zdt");
|
||||||
|
|
||||||
|
const Dimensions = @import("../../Dimensions.zig");
|
||||||
|
const renderer = @import("../../renderer.zig");
|
||||||
|
|
||||||
|
pub const efa = @import("efa.zig");
|
||||||
|
|
||||||
|
const template = @import("template/root.zig");
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
dimensions: Dimensions,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, dimensions: Dimensions) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.dimensions = dimensions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RenderOptions = struct {
|
||||||
|
now: *const zdt.Datetime,
|
||||||
|
tz: *const zdt.Timezone,
|
||||||
|
max_items: usize,
|
||||||
|
show_operator: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn render(self: *const Self, efa_dm_resp: *const efa.DmResponse, options: RenderOptions) ![]const u8 {
|
||||||
|
const now_local = try options.now.tzConvert(.{ .tz = options.tz });
|
||||||
|
|
||||||
|
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
const departures = try arena.allocator().alloc(template.Departure, efa_dm_resp.stopEvents.len);
|
||||||
|
var dep_i: usize = 0;
|
||||||
|
for (efa_dm_resp.stopEvents) |dep| {
|
||||||
|
const dp_timetable = try zdt.Datetime.fromISO8601(dep.departureTimeBaseTimetable);
|
||||||
|
const dp_timetable_local = try dp_timetable.tzConvert(.{ .tz = options.tz });
|
||||||
|
|
||||||
|
const dp_planned = try zdt.Datetime.fromISO8601(
|
||||||
|
dep.departureTimeEstimated orelse dep.departureTimePlanned,
|
||||||
|
);
|
||||||
|
const dp_planned_local = try dp_planned.tzConvert(.{ .tz = options.tz });
|
||||||
|
|
||||||
|
const diff_min: i32 = @intFromFloat(dp_planned_local.diff(dp_timetable_local).totalMinutes());
|
||||||
|
const on_time: ?u32 = if (diff_min < 0) @abs(diff_min) else null;
|
||||||
|
const delayed: ?u32 = if (diff_min > 0) @abs(diff_min) else null;
|
||||||
|
|
||||||
|
const date: ?[]const u8 = if (dp_planned_local.dayOfYear() != now_local.dayOfYear())
|
||||||
|
try std.fmt.allocPrint(arena.allocator(), "{d}.{d}.{d}", .{
|
||||||
|
dp_planned_local.day,
|
||||||
|
dp_planned_local.month,
|
||||||
|
dp_planned_local.year,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
|
var infos: ?[]const []const u8 = null;
|
||||||
|
if (dep.hints) |hints| {
|
||||||
|
const arr = try arena.allocator().alloc([]const u8, hints.len + 1);
|
||||||
|
var i: usize = 0;
|
||||||
|
for (hints) |hint| {
|
||||||
|
if (std.mem.eql(u8, hint.type, "Timetable"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
arr[i] = hint.content;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
infos = arr[0..i];
|
||||||
|
}
|
||||||
|
|
||||||
|
departures[dep_i] = template.Departure{
|
||||||
|
.time = try std.fmt.allocPrint(
|
||||||
|
arena.allocator(),
|
||||||
|
"{d}:{d:0>2}",
|
||||||
|
.{ dp_timetable_local.hour, dp_timetable_local.minute },
|
||||||
|
),
|
||||||
|
.time_unix = dp_timetable_local.toUnix(.second),
|
||||||
|
.line = dep.transportation.name,
|
||||||
|
.transportation_product_class = dep.transportation.product.class,
|
||||||
|
.operator = if (dep.transportation.operator) |op| op.name else null,
|
||||||
|
.destination = dep.transportation.destination.name,
|
||||||
|
.platform = dep.location.properties.platform,
|
||||||
|
.date = date,
|
||||||
|
.on_time = on_time,
|
||||||
|
.delayed = delayed,
|
||||||
|
.cancelled = dep.isCancelled,
|
||||||
|
.information = infos,
|
||||||
|
};
|
||||||
|
|
||||||
|
dep_i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std.mem.sort(
|
||||||
|
template.Departure,
|
||||||
|
departures,
|
||||||
|
{},
|
||||||
|
template.Departure.cmpByTimeUnix,
|
||||||
|
);
|
||||||
|
|
||||||
|
const context = template.Context{
|
||||||
|
.station = efa_dm_resp.locations[0].name,
|
||||||
|
.time = try std.fmt.allocPrint(
|
||||||
|
arena.allocator(),
|
||||||
|
"{d}:{d:0>2}",
|
||||||
|
.{ now_local.hour, now_local.minute },
|
||||||
|
),
|
||||||
|
.departures = departures[0..@min(departures.len, options.max_items)],
|
||||||
|
.show_operator = options.show_operator,
|
||||||
|
};
|
||||||
|
|
||||||
|
const escaped_context = try context.escape(arena.allocator());
|
||||||
|
|
||||||
|
var tmp_file = try temp.TempFile.create(arena.allocator(), .{
|
||||||
|
.pattern = "departures-*.html",
|
||||||
|
});
|
||||||
|
defer tmp_file.deinit();
|
||||||
|
const path = try tmp_file.parent_dir.realpathAlloc(arena.allocator(), tmp_file.basename);
|
||||||
|
{
|
||||||
|
const file = try tmp_file.open(.{ .mode = .write_only });
|
||||||
|
defer file.close();
|
||||||
|
const writer = file.writer();
|
||||||
|
try template.render(&escaped_context, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return try renderer.render(self.allocator, path, self.dimensions);
|
||||||
|
}
|
194
src/eink_feed_render/apps/Departures/template/css/fonts.css
Normal file
104
src/eink_feed_render/apps/Departures/template/css/style.css
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
body {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vw;
|
||||||
|
margin: 0;
|
||||||
|
/*font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;*/
|
||||||
|
font-family: "Noto Sans", sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-variation-settings: "wdth" 100;
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor {
|
||||||
|
max-width: 90vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 1%;
|
||||||
|
/* border-bottom: 0.2rem solid black; */
|
||||||
|
/* padding-bottom: 0.25em; */
|
||||||
|
width: 100%;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-pack: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .header-title {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 80%;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .header-time {
|
||||||
|
text-align: right;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 20%;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.departures {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.departures th,
|
||||||
|
.departures td {
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
text-align: left;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.departures .data td {
|
||||||
|
padding-top: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.departures .border td {
|
||||||
|
padding-top: 0.35em;
|
||||||
|
padding-bottom: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.departures th {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.departures td {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.departures .border {
|
||||||
|
border-bottom: 0.2rem solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operator {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-time {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-date {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
BIN
src/eink_feed_render/apps/Departures/template/icons/img/boat.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/eink_feed_render/apps/Departures/template/icons/img/bus.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.5 KiB |
BIN
src/eink_feed_render/apps/Departures/template/icons/img/tram.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
63
src/eink_feed_render/apps/Departures/template/icons/root.zig
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const efa = @import("../../efa.zig");
|
||||||
|
|
||||||
|
pub fn iconFromTransportationProductClass(class: efa.ApiTransportationProductClass) ?[]const u8 {
|
||||||
|
return switch (class) {
|
||||||
|
.zug,
|
||||||
|
.s_bahn,
|
||||||
|
.zug_nv,
|
||||||
|
.zug_fv,
|
||||||
|
.zug_fv_m_zuschlag,
|
||||||
|
.zug_fv_m_spez_fpr,
|
||||||
|
.zug_shuttle,
|
||||||
|
=> train,
|
||||||
|
|
||||||
|
.u_bahn => subway,
|
||||||
|
|
||||||
|
.stadtbahn, .strassen_trambahn => tram,
|
||||||
|
|
||||||
|
.stadtbus,
|
||||||
|
.regionalbus,
|
||||||
|
.schnellbus,
|
||||||
|
.buergerbus,
|
||||||
|
.rufbus_liniengebunden,
|
||||||
|
.rufbus,
|
||||||
|
.anruf_sammel_taxi,
|
||||||
|
=> bus,
|
||||||
|
|
||||||
|
.seil_zahnradbahn => gondola,
|
||||||
|
|
||||||
|
.schiff => boat,
|
||||||
|
|
||||||
|
.sonstige => null,
|
||||||
|
|
||||||
|
.flugzeug => flight,
|
||||||
|
|
||||||
|
.sev => no_transfer,
|
||||||
|
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encodePng(comptime path: []const u8) []const u8 {
|
||||||
|
const raw = @embedFile(path);
|
||||||
|
const len = std.base64.standard.Encoder.calcSize(raw.len);
|
||||||
|
var buf: [len]u8 = undefined;
|
||||||
|
{
|
||||||
|
@setEvalBranchQuota(1_000_000);
|
||||||
|
_ = std.base64.standard.Encoder.encode(&buf, raw);
|
||||||
|
}
|
||||||
|
const final = buf;
|
||||||
|
return &final;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const train = encodePng("img/train.png");
|
||||||
|
pub const subway = encodePng("img/subway.png");
|
||||||
|
pub const tram = encodePng("img/tram.png");
|
||||||
|
pub const bus = encodePng("img/bus.png");
|
||||||
|
pub const shuttle = encodePng("img/shuttle.png");
|
||||||
|
pub const gondola = encodePng("img/gondola.png");
|
||||||
|
pub const flight = encodePng("img/flight.png");
|
||||||
|
pub const boat = encodePng("img/boat.png");
|
||||||
|
pub const no_transfer = encodePng("img/no_transfer.png");
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M152-80h-32v-80h32q48 0 91.5-10.5T341-204q38 19 66.5 31.5T480-160q44 0 72.5-12.5T619-204q53 23 97.5 33.5T809-160h31v80h-31q-49 0-95.5-9T622-116q-40 19-73 27t-69 8q-36 0-68.5-8T339-116q-45 18-91.5 27T152-80Zm328-160q-60 0-105-40l-45-40q-27 27-60.5 46T198-247l-85-273q-5-17 3-31t25-19l59-16v-134q0-33 23.5-56.5T280-800h100v-80h200v80h100q33 0 56.5 23.5T760-720v134l59 16q17 5 25 19t3 31l-85 273q-38-8-71.5-27T630-320l-45 40q-45 40-105 40Zm2-80q31 0 55-20.5t44-43.5l46-53 41 42q11 11 22.5 20.5T713-355l46-149-279-73-278 73 46 149q11-10 22.5-19.5T293-395l41-42 46 53q20 24 45 44t57 20ZM280-607l200-53 200 53v-113H280v113Zm201 158Z"/></svg>
|
After Width: | Height: | Size: 751 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M240-120q-17 0-28.5-11.5T200-160v-82q-18-20-29-44.5T160-340v-380q0-83 77-121.5T480-880q172 0 246 37t74 123v380q0 29-11 53.5T760-242v82q0 17-11.5 28.5T720-120h-40q-17 0-28.5-11.5T640-160v-40H320v40q0 17-11.5 28.5T280-120h-40Zm242-640h224-448 224Zm158 280H240h480-80Zm-400-80h480v-120H240v120Zm100 240q25 0 42.5-17.5T400-380q0-25-17.5-42.5T340-440q-25 0-42.5 17.5T280-380q0 25 17.5 42.5T340-320Zm280 0q25 0 42.5-17.5T680-380q0-25-17.5-42.5T620-440q-25 0-42.5 17.5T560-380q0 25 17.5 42.5T620-320ZM258-760h448q-15-17-64.5-28.5T482-800q-107 0-156.5 12.5T258-760Zm62 480h320q33 0 56.5-23.5T720-360v-120H240v120q0 33 23.5 56.5T320-280Z"/></svg>
|
After Width: | Height: | Size: 753 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M280-80v-100l120-84v-144L80-280v-120l320-224v-176q0-33 23.5-56.5T480-880q33 0 56.5 23.5T560-800v176l320 224v120L560-408v144l120 84v100l-200-60-200 60Z"/></svg>
|
After Width: | Height: | Size: 275 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M200-120q-33 0-56.5-23.5T120-200v-240q0-66 47-113t113-47h160v-109L40-600v-80l205-56q-2-5-3.5-11t-1.5-13q0-25 17.5-42.5T300-820q23 0 40 15t19 38l81-22v-51h80v29l86-23q-3-6-4.5-12.5T600-860q0-25 17.5-42.5T660-920q23 0 40.5 16t19.5 39l200-55v80L520-731v131h160q66 0 113 47t47 113v240q0 33-23.5 56.5T760-120H200Zm0-80h560v-80H200v80Zm0-160h133v-160h-53q-33 0-56.5 23.5T200-440v80Zm213 0h133v-160H413v160Zm214 0h133v-80q0-33-23.5-56.5T680-520h-53v160ZM200-200v-80 80Z"/></svg>
|
After Width: | Height: | Size: 587 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M819-28 652-195h114v35q0 17-11.5 28.5T726-120h-46q-17 0-28.5-11.5T640-160v-40H320v40q0 17-11.5 28.5T280-120h-40q-17 0-28.5-11.5T200-160v-82q-18-20-29-44.5T160-340v-347L27-820l57-57L876-85l-57 57ZM320-280h247L367-480H240v120q0 33 23.5 56.5T320-280Zm469-6-69-69v-125H595l-80-80h205v-120H395l-80-80h391q-15-17-64.5-28.5T482-800q-71 0-115.5 6T296-779l-61-61q39-20 99.5-30T480-880q172 0 246 37t74 123v380q0 14-3 27.5t-8 26.5Zm-449-34q25 0 42.5-17.5T400-380q0-25-17.5-42.5T340-440q-25 0-42.5 17.5T280-380q0 25 17.5 42.5T340-320ZM240-560h47l-47-47v47Zm75-200h391-391Zm52 280Zm228 0Z"/></svg>
|
After Width: | Height: | Size: 700 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M240-200q-50 0-85-35t-35-85H40v-360q0-33 23.5-56.5T120-760h560l240 240v200h-80q0 50-35 85t-85 35q-50 0-85-35t-35-85H360q0 50-35 85t-85 35Zm360-360h160L640-680h-40v120Zm-240 0h160v-120H360v120Zm-240 0h160v-120H120v120Zm120 290q21 0 35.5-14.5T290-320q0-21-14.5-35.5T240-370q-21 0-35.5 14.5T190-320q0 21 14.5 35.5T240-270Zm480 0q21 0 35.5-14.5T770-320q0-21-14.5-35.5T720-370q-21 0-35.5 14.5T670-320q0 21 14.5 35.5T720-270ZM120-400h32q17-18 39-29t49-11q27 0 49 11t39 29h304q17-18 39-29t49-11q27 0 49 11t39 29h32v-80H120v80Zm720-80H120h720Z"/></svg>
|
After Width: | Height: | Size: 660 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M80-80v-526q0-85 44-147.5T248-848q54-21 115-26.5t117-5.5q56 0 117 5.5T712-848q80 32 124 94.5T880-606v526H80Zm284-80h230l-60-60H424l-60 60Zm-64-280h360v-160H300v160Zm320 140q17 0 28.5-11.5T660-340q0-17-11.5-28.5T620-380q-17 0-28.5 11.5T580-340q0 17 11.5 28.5T620-300Zm-280 0q17 0 28.5-11.5T380-340q0-17-11.5-28.5T340-380q-17 0-28.5 11.5T300-340q0 17 11.5 28.5T340-300ZM160-160h140v-20l42-42q-44-6-73-39.5T240-340v-260q0-78 74.5-99T480-720q100 0 170 21t70 99v260q0 45-29 78.5T618-222l42 42v20h140v-446q0-60-29.5-102.5T682-774q-44-17-97.5-21.5T480-800q-51 0-104.5 4.5T278-774q-59 23-88.5 65.5T160-606v446Zm0 0h640-640Z"/></svg>
|
After Width: | Height: | Size: 740 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M160-340v-380q0-53 27.5-84.5t72.5-48q45-16.5 102.5-22T480-880q66 0 124.5 5.5t102 22q43.5 16.5 68.5 48t25 84.5v380q0 59-40.5 99.5T660-200l60 60v20h-80l-80-80H400l-80 80h-80v-20l60-60q-59 0-99.5-40.5T160-340Zm320-460q-106 0-155 12.5T258-760h448q-15-17-64.5-28.5T480-800ZM240-560h200v-120H240v120Zm420 80H240h480-60Zm-140-80h200v-120H520v120ZM340-320q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm280 0q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm-320 40h360q26 0 43-17t17-43v-140H240v140q0 26 17 43t43 17Zm180-480h226-448 222Z"/></svg>
|
After Width: | Height: | Size: 703 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M160-260v-380q0-97 85-127t195-33l30-60H280v-60h400v60H550l-30 60q119 3 199.5 32.5T800-640v380q0 59-40.5 99.5T660-120l60 60v20h-80l-80-80H400l-80 80h-80v-20l60-60q-59 0-99.5-40.5T160-260Zm500-140H240h480-60ZM480-240q25 0 42.5-17.5T540-300q0-25-17.5-42.5T480-360q-25 0-42.5 17.5T420-300q0 25 17.5 42.5T480-240Zm-2-440h228-450 222ZM240-480h480v-120H240v120Zm60 280h360q26 0 43-17t17-43v-140H240v140q0 26 17 43t43 17Zm178-520q-134 0-172 14.5T256-680h450q-12-14-52-27t-176-13Z"/></svg>
|
After Width: | Height: | Size: 596 B |
264
src/eink_feed_render/apps/Departures/template/root.zig
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const escaper = @import("../../../escaper.zig");
|
||||||
|
const efa = @import("../efa.zig");
|
||||||
|
|
||||||
|
const icons = @import("icons/root.zig");
|
||||||
|
|
||||||
|
pub const Departure = struct {
|
||||||
|
time: []const u8,
|
||||||
|
time_unix: i128,
|
||||||
|
line: []const u8,
|
||||||
|
transportation_product_class: efa.ApiTransportationProductClass,
|
||||||
|
operator: ?[]const u8,
|
||||||
|
destination: []const u8,
|
||||||
|
platform: ?[]const u8,
|
||||||
|
date: ?[]const u8,
|
||||||
|
on_time: ?u32,
|
||||||
|
delayed: ?u32,
|
||||||
|
cancelled: bool,
|
||||||
|
information: ?[]const []const u8,
|
||||||
|
|
||||||
|
pub fn cmpByTimeUnix(context: void, a: Departure, b: Departure) bool {
|
||||||
|
return std.sort.asc(i128)(context, a.time_unix, b.time_unix);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn escape(self: *const Departure, allocator: std.mem.Allocator) !Departure {
|
||||||
|
var copy: Departure = undefined;
|
||||||
|
|
||||||
|
const time = try escaper.escapeHtml(allocator, self.time);
|
||||||
|
errdefer if (time) |s| allocator.free(s);
|
||||||
|
copy.time = time orelse self.time;
|
||||||
|
|
||||||
|
const line = try escaper.escapeHtml(allocator, self.line);
|
||||||
|
errdefer if (line) |s| allocator.free(s);
|
||||||
|
copy.line = line orelse self.line;
|
||||||
|
|
||||||
|
copy.transportation_product_class = self.transportation_product_class;
|
||||||
|
|
||||||
|
copy.operator = null;
|
||||||
|
if (self.operator) |str| {
|
||||||
|
const operator = try escaper.escapeHtml(allocator, str);
|
||||||
|
errdefer if (operator) |s| allocator.free(s);
|
||||||
|
copy.operator = operator orelse str;
|
||||||
|
}
|
||||||
|
|
||||||
|
const destination = try escaper.escapeHtml(allocator, self.destination);
|
||||||
|
errdefer if (destination) |s| allocator.free(s);
|
||||||
|
copy.destination = destination orelse self.destination;
|
||||||
|
|
||||||
|
copy.platform = null;
|
||||||
|
if (self.platform) |str| {
|
||||||
|
const platform = try escaper.escapeHtml(allocator, str);
|
||||||
|
errdefer if (platform) |s| allocator.free(s);
|
||||||
|
copy.platform = platform orelse str;
|
||||||
|
}
|
||||||
|
|
||||||
|
copy.date = null;
|
||||||
|
if (self.date) |str| {
|
||||||
|
const date = try escaper.escapeHtml(allocator, str);
|
||||||
|
errdefer if (date) |s| allocator.free(s);
|
||||||
|
copy.date = date orelse str;
|
||||||
|
}
|
||||||
|
|
||||||
|
copy.on_time = self.on_time;
|
||||||
|
copy.delayed = self.delayed;
|
||||||
|
copy.cancelled = self.cancelled;
|
||||||
|
|
||||||
|
copy.information = null;
|
||||||
|
if (self.information) |arr| {
|
||||||
|
const infos = try allocator.alloc([]const u8, arr.len);
|
||||||
|
for (arr, 0..) |str, i| {
|
||||||
|
const information = try escaper.escapeHtml(allocator, str);
|
||||||
|
errdefer if (information) |s| allocator.free(s);
|
||||||
|
infos[i] = information orelse str;
|
||||||
|
}
|
||||||
|
copy.information = infos;
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Context = struct {
|
||||||
|
station: []const u8,
|
||||||
|
time: []const u8,
|
||||||
|
departures: []const Departure,
|
||||||
|
show_operator: bool,
|
||||||
|
|
||||||
|
pub fn escape(self: *const Context, allocator: std.mem.Allocator) !Context {
|
||||||
|
var copy: Context = undefined;
|
||||||
|
|
||||||
|
const station = try escaper.escapeHtml(allocator, self.station);
|
||||||
|
errdefer if (station) |s| allocator.free(s);
|
||||||
|
copy.station = station orelse self.station;
|
||||||
|
|
||||||
|
const time = try escaper.escapeHtml(allocator, self.time);
|
||||||
|
errdefer if (time) |s| allocator.free(s);
|
||||||
|
copy.time = time orelse self.time;
|
||||||
|
|
||||||
|
const departures = try allocator.alloc(Departure, self.departures.len);
|
||||||
|
errdefer allocator.free(copy.departures);
|
||||||
|
for (self.departures, 0..) |departure, i| {
|
||||||
|
const escaped = try departure.escape(allocator);
|
||||||
|
departures[i] = escaped;
|
||||||
|
}
|
||||||
|
copy.departures = departures;
|
||||||
|
|
||||||
|
copy.show_operator = self.show_operator;
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn render(context: *const Context, writer: anytype) !void {
|
||||||
|
try writer.writeAll(
|
||||||
|
\\<!DOCTYPE html>
|
||||||
|
\\<html lang="de">
|
||||||
|
\\ <head>
|
||||||
|
\\ <meta charset="UTF-8" />
|
||||||
|
\\ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
);
|
||||||
|
try writer.print(
|
||||||
|
\\<style>{[fonts_css]s}</style>
|
||||||
|
\\<style>{[style_css]s}</style>
|
||||||
|
,
|
||||||
|
.{
|
||||||
|
.fonts_css = @embedFile("css/fonts.css"),
|
||||||
|
.style_css = @embedFile("css/style.css"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try writer.writeAll(
|
||||||
|
\\</head>
|
||||||
|
\\<body>
|
||||||
|
\\ <div class="monitor">
|
||||||
|
\\ <header class="header">
|
||||||
|
);
|
||||||
|
try writer.print(
|
||||||
|
\\<h1 class="header-title">Abfahrten: {[station]s}</h1>
|
||||||
|
\\<h2 class="header-time">{[time]s}</h2>
|
||||||
|
, .{ .station = context.station, .time = context.time });
|
||||||
|
try writer.writeAll(
|
||||||
|
\\</header>
|
||||||
|
\\ <table style="width: 100%" class="departures">
|
||||||
|
\\ <colgroup>
|
||||||
|
\\ <col span="1" style="width: 15%;">
|
||||||
|
\\ <col span="1" style="width: 10%;">
|
||||||
|
\\ <col span="1" style="width: 30%;">
|
||||||
|
\\ <col span="1" style="width: 30%;">
|
||||||
|
\\ <col span="1" style="width: 15%;">
|
||||||
|
\\ </colgroup>
|
||||||
|
\\ <thead>
|
||||||
|
\\ <tr class="border">
|
||||||
|
\\ <th>Zeit</th>
|
||||||
|
\\ <th colspan="2">Linie</th>
|
||||||
|
\\ <th>Ziel</th>
|
||||||
|
\\ <th style="text-align: right;">Gleis/<wbr />Steig</th>
|
||||||
|
\\ </tr>
|
||||||
|
\\ </thead>
|
||||||
|
\\ <tbody>
|
||||||
|
);
|
||||||
|
|
||||||
|
for (context.departures) |departure| {
|
||||||
|
try writer.print(
|
||||||
|
\\<tr class="data {[cancelled]s} {[border]s}">
|
||||||
|
,
|
||||||
|
.{
|
||||||
|
.cancelled = if (departure.cancelled) "status-cancelled" else "",
|
||||||
|
.border = if (departure.information == null) "border" else "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
try writer.print("<td>{s}", .{departure.time});
|
||||||
|
if (departure.on_time) |n| {
|
||||||
|
try writer.print(
|
||||||
|
\\ <wbr /><span class="status-time">(-{d})</span>
|
||||||
|
, .{n});
|
||||||
|
}
|
||||||
|
if (departure.delayed) |n| {
|
||||||
|
try writer.print(
|
||||||
|
\\ <wbr /><span class="status-time">(+{d})</span>
|
||||||
|
, .{n});
|
||||||
|
}
|
||||||
|
if (departure.date) |date| {
|
||||||
|
try writer.print(
|
||||||
|
\\<br /><span class="status-date">({s})</span>
|
||||||
|
,
|
||||||
|
.{date},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icons.iconFromTransportationProductClass(departure.transportation_product_class)) |img| {
|
||||||
|
try writer.print(
|
||||||
|
\\ <td>
|
||||||
|
\\ <img class="icon" src="data:image/png;charset=utf-8;base64,{[img]s}" />
|
||||||
|
\\ </td>
|
||||||
|
,
|
||||||
|
.{ .img = img },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try writer.writeAll(
|
||||||
|
\\<td></td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.print(
|
||||||
|
\\<td><span>{[line]s}<span>
|
||||||
|
,
|
||||||
|
.{ .line = departure.line },
|
||||||
|
);
|
||||||
|
if (context.show_operator) {
|
||||||
|
if (departure.operator) |operator| {
|
||||||
|
try writer.print(
|
||||||
|
\\<br /><span class="operator">{[operator]s}</span>
|
||||||
|
,
|
||||||
|
.{ .operator = operator },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try writer.writeAll(
|
||||||
|
\\</td>
|
||||||
|
);
|
||||||
|
|
||||||
|
try writer.print(
|
||||||
|
\\ <td>{[destination]s}</td>
|
||||||
|
\\ <td style="text-align: right;">{[platform]s}</td>
|
||||||
|
\\</tr>
|
||||||
|
,
|
||||||
|
.{
|
||||||
|
.destination = departure.destination,
|
||||||
|
.platform = departure.platform orelse "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (departure.information) |infos| {
|
||||||
|
try writer.print(
|
||||||
|
\\<tr class="border info {[cancelled]s}">
|
||||||
|
\\ <td></td>
|
||||||
|
\\ <td></td>
|
||||||
|
\\ <td colspan="3">
|
||||||
|
,
|
||||||
|
.{
|
||||||
|
.cancelled = if (departure.cancelled) "status-cancelled" else "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for (infos, 0..) |str, i| {
|
||||||
|
try writer.print("<span>{s}</span>", .{str});
|
||||||
|
if (i < infos.len - 1)
|
||||||
|
try writer.writeAll("<br />");
|
||||||
|
}
|
||||||
|
try writer.writeAll(
|
||||||
|
\\ </td>
|
||||||
|
\\</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.writeAll(
|
||||||
|
\\ </tbody>
|
||||||
|
\\ </table>
|
||||||
|
\\ </div>
|
||||||
|
\\ </body>
|
||||||
|
\\</html>
|
||||||
|
);
|
||||||
|
}
|
1
src/eink_feed_render/apps/root.zig
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub const Departures = @import("Departures/root.zig");
|
35
src/eink_feed_render/escaper.zig
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const c = @cImport({
|
||||||
|
@cInclude("houdini.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn escapeHtml(allocator: std.mem.Allocator, data: []const u8) !?[]const u8 {
|
||||||
|
var buf: c.gh_buf = undefined;
|
||||||
|
c.gh_buf_init(&buf, data.len);
|
||||||
|
defer c.gh_buf_free(&buf);
|
||||||
|
|
||||||
|
const res = c.houdini_escape_html(&buf, @ptrCast(data), data.len);
|
||||||
|
if (res == 0) return null;
|
||||||
|
|
||||||
|
const escaped = try allocator.alloc(u8, c.gh_buf_len(&buf));
|
||||||
|
errdefer allocator.free(escaped);
|
||||||
|
@memcpy(escaped, c.gh_buf_cstr(&buf)[0..escaped.len]);
|
||||||
|
|
||||||
|
return escaped;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn escapeUriComponent(allocator: std.mem.Allocator, data: []const u8) !?[]const u8 {
|
||||||
|
var buf: c.gh_buf = undefined;
|
||||||
|
c.gh_buf_init(&buf, data.len);
|
||||||
|
defer c.gh_buf_free(&buf);
|
||||||
|
|
||||||
|
const res = c.houdini_escape_uri_component(&buf, @ptrCast(data), data.len);
|
||||||
|
if (res == 0) return null;
|
||||||
|
|
||||||
|
const escaped = try allocator.alloc(u8, c.gh_buf_len(&buf));
|
||||||
|
errdefer allocator.free(escaped);
|
||||||
|
@memcpy(escaped, c.gh_buf_cstr(&buf)[0..escaped.len]);
|
||||||
|
|
||||||
|
return escaped;
|
||||||
|
}
|
101
src/eink_feed_render/main/departures.zig
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const clap = @import("clap");
|
||||||
|
const zdt = @import("zdt");
|
||||||
|
|
||||||
|
const render = @import("eink_feed_render");
|
||||||
|
|
||||||
|
pub const std_options: std.Options = .{
|
||||||
|
.log_level = if (builtin.mode == .Debug) .debug else .info,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{
|
||||||
|
.thread_safe = true,
|
||||||
|
}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
{
|
||||||
|
const params = comptime clap.parseParamsComptime(
|
||||||
|
\\-h, --help Display this help and exit.
|
||||||
|
\\--url <str> URL of eink-feed server.
|
||||||
|
\\--channel <u32> Channel ID.
|
||||||
|
\\--efa <str> URL of EFA server.
|
||||||
|
\\--stop <str> Stop ID.
|
||||||
|
\\--tz <str> Time zone.
|
||||||
|
\\--max <usize> Max number of departures listed.
|
||||||
|
\\--show-operator Show operator.
|
||||||
|
\\--stopfinder <str> Search for stops on the EFA server with the given search query.
|
||||||
|
);
|
||||||
|
|
||||||
|
var diag = clap.Diagnostic{};
|
||||||
|
var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{
|
||||||
|
.diagnostic = &diag,
|
||||||
|
.allocator = allocator,
|
||||||
|
}) catch |err| {
|
||||||
|
diag.report(std.io.getStdErr().writer(), err) catch {};
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
defer res.deinit();
|
||||||
|
|
||||||
|
if (res.args.help != 0 or
|
||||||
|
res.args.efa == null or
|
||||||
|
((res.args.stop == null or
|
||||||
|
res.args.tz == null or
|
||||||
|
res.args.url == null or
|
||||||
|
res.args.channel == null) and
|
||||||
|
res.args.stopfinder == null))
|
||||||
|
{
|
||||||
|
const writer = std.io.getStdErr().writer();
|
||||||
|
try writer.writeAll("eink-feed-render-departures\n");
|
||||||
|
return clap.help(writer, clap.Help, ¶ms, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
const efa = try render.apps.Departures.efa.Client.init(allocator, res.args.efa.?);
|
||||||
|
defer efa.deinit();
|
||||||
|
|
||||||
|
if (res.args.stopfinder) |query| {
|
||||||
|
try efa.findStops(query);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tz = try zdt.Timezone.fromTzdata(res.args.tz.?, allocator);
|
||||||
|
defer tz.deinit();
|
||||||
|
|
||||||
|
render.renderer.init();
|
||||||
|
defer render.renderer.deinit();
|
||||||
|
|
||||||
|
const feed_client = try render.FeedClient.init(allocator, res.args.url.?);
|
||||||
|
defer feed_client.deinit();
|
||||||
|
|
||||||
|
const channel = try feed_client.getChannel(res.args.channel.?);
|
||||||
|
defer channel.deinit();
|
||||||
|
const dim = render.Dimensions{
|
||||||
|
.width = channel.value.display.width,
|
||||||
|
.height = channel.value.display.height,
|
||||||
|
};
|
||||||
|
const orientation = channel.value.display.orientation.toInternal();
|
||||||
|
const departures = render.apps.Departures.init(allocator, dim);
|
||||||
|
|
||||||
|
const stop_id: []const u8 = res.args.stop.?;
|
||||||
|
|
||||||
|
const now = try zdt.Datetime.now(.{ .tz = &tz });
|
||||||
|
const efa_departures = try efa.getDepartures(stop_id);
|
||||||
|
defer efa_departures.deinit();
|
||||||
|
|
||||||
|
const png = try departures.render(&efa_departures.value, .{
|
||||||
|
.now = &now,
|
||||||
|
.tz = &tz,
|
||||||
|
.max_items = res.args.max orelse 15,
|
||||||
|
.show_operator = res.args.@"show-operator" != 0,
|
||||||
|
});
|
||||||
|
defer allocator.free(png);
|
||||||
|
try feed_client.uploadFrame(
|
||||||
|
channel.value.id,
|
||||||
|
orientation,
|
||||||
|
png,
|
||||||
|
"image/png",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
90
src/eink_feed_render/renderer.zig
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Dimensions = @import("Dimensions.zig");
|
||||||
|
|
||||||
|
const c = @cImport({
|
||||||
|
@cInclude("wkhtmltox/image.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
const log = std.log.scoped(.renderer);
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
Wkhtml,
|
||||||
|
};
|
||||||
|
|
||||||
|
var wkhtmlInit = false;
|
||||||
|
|
||||||
|
pub fn init() void {
|
||||||
|
if (!wkhtmlInit) {
|
||||||
|
_ = c.wkhtmltoimage_init(0);
|
||||||
|
wkhtmlInit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit() void {
|
||||||
|
_ = c.wkhtmltoimage_deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wkhtmlProgressChanged(converter: ?*c.wkhtmltoimage_converter, progress: c_int) callconv(.C) void {
|
||||||
|
_ = converter;
|
||||||
|
_ = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wkhtmlPhaseChanged(converter: ?*c.wkhtmltoimage_converter) callconv(.C) void {
|
||||||
|
_ = converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wkhtmlError(converter: ?*c.wkhtmltoimage_converter, msg: [*c]const u8) callconv(.C) void {
|
||||||
|
_ = converter;
|
||||||
|
log.err("Convert error: {s}", .{msg});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wkhtmlWarning(converter: ?*c.wkhtmltoimage_converter, msg: [*c]const u8) callconv(.C) void {
|
||||||
|
_ = converter;
|
||||||
|
log.warn("Convert warning: {s}", .{msg});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(allocator: std.mem.Allocator, input_path: []const u8, dimensions: Dimensions) ![]const u8 {
|
||||||
|
const input = try allocator.dupeZ(u8, input_path);
|
||||||
|
defer allocator.free(input);
|
||||||
|
|
||||||
|
const width = try std.fmt.allocPrintZ(allocator, "{d}", .{dimensions.width});
|
||||||
|
defer allocator.free(width);
|
||||||
|
const height = try std.fmt.allocPrintZ(allocator, "{d}", .{dimensions.height});
|
||||||
|
defer allocator.free(height);
|
||||||
|
|
||||||
|
const gs = c.wkhtmltoimage_create_global_settings().?;
|
||||||
|
|
||||||
|
_ = c.wkhtmltoimage_set_global_setting(gs, "web.loadImages", "true");
|
||||||
|
|
||||||
|
_ = c.wkhtmltoimage_set_global_setting(gs, "screenWidth", width);
|
||||||
|
_ = c.wkhtmltoimage_set_global_setting(gs, "smartWidth", "false");
|
||||||
|
_ = c.wkhtmltoimage_set_global_setting(gs, "crop.width", width);
|
||||||
|
_ = c.wkhtmltoimage_set_global_setting(gs, "crop.height", height);
|
||||||
|
|
||||||
|
_ = c.wkhtmltoimage_set_global_setting(gs, "in", input);
|
||||||
|
_ = c.wkhtmltoimage_set_global_setting(gs, "fmt", "png");
|
||||||
|
_ = c.wkhtmltoimage_set_global_setting(gs, "transparent", "false");
|
||||||
|
_ = c.wkhtmltoimage_set_global_setting(gs, "quality", "100");
|
||||||
|
|
||||||
|
const converter = c.wkhtmltoimage_create_converter(gs, 0).?;
|
||||||
|
defer c.wkhtmltoimage_destroy_converter(converter);
|
||||||
|
|
||||||
|
c.wkhtmltoimage_set_progress_changed_callback(converter, wkhtmlProgressChanged);
|
||||||
|
c.wkhtmltoimage_set_phase_changed_callback(converter, wkhtmlPhaseChanged);
|
||||||
|
c.wkhtmltoimage_set_error_callback(converter, wkhtmlError);
|
||||||
|
c.wkhtmltoimage_set_warning_callback(converter, wkhtmlWarning);
|
||||||
|
|
||||||
|
if (c.wkhtmltoimage_convert(converter) == 0) {
|
||||||
|
log.err("Conversion failed", .{});
|
||||||
|
return Error.Wkhtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data: [*c]const u8 = undefined;
|
||||||
|
const len = c.wkhtmltoimage_get_output(converter, &data);
|
||||||
|
const img = data[0..@intCast(len)];
|
||||||
|
|
||||||
|
return try allocator.dupe(u8, img);
|
||||||
|
}
|
4
src/eink_feed_render/root.zig
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub const apps = @import("apps/root.zig");
|
||||||
|
pub const renderer = @import("renderer.zig");
|
||||||
|
pub const FeedClient = @import("FeedClient.zig");
|
||||||
|
pub const Dimensions = @import("Dimensions.zig");
|
77
src/eink_feed_server/dispatchers/FrameUpdate.zig
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const protocol = @import("eink_feed_protocol");
|
||||||
|
|
||||||
|
const ResultArena = @import("../result.zig").ResultArena;
|
||||||
|
const msg_queue = @import("../msg_queue/root.zig");
|
||||||
|
const models = @import("../models/root.zig");
|
||||||
|
const web = @import("../web/root.zig");
|
||||||
|
|
||||||
|
const WebsocketHandler = web.endpoints.ws.Display.WebsocketHandler;
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
const log = std.log.scoped(.frame_update_dispatcher);
|
||||||
|
|
||||||
|
/// Frame update dispatcher config
|
||||||
|
pub const Config = struct {
|
||||||
|
/// Queue capacity
|
||||||
|
capacity: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
/// Dispatcher config
|
||||||
|
config: *const Config,
|
||||||
|
|
||||||
|
/// Frame update queue
|
||||||
|
queue: *msg_queue.MsgQueueUnmanaged(ResultArena(models.FrameUpdateMsg)),
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
config: *const Config,
|
||||||
|
queue: *msg_queue.MsgQueueUnmanaged(ResultArena(models.FrameUpdateMsg)),
|
||||||
|
) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.config = config,
|
||||||
|
.queue = queue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(self: *Self) !void {
|
||||||
|
const buf = try self.allocator.alloc(ResultArena(models.FrameUpdateMsg), self.queue.capacity());
|
||||||
|
defer self.allocator.free(buf);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const updates = try self.queue.dequeueAll(buf);
|
||||||
|
for (updates) |*result| {
|
||||||
|
defer result.deinit();
|
||||||
|
const update = result.value;
|
||||||
|
|
||||||
|
const min_len: u32 = update.display.height * update.display.width;
|
||||||
|
if (update.frame.len < min_len) continue;
|
||||||
|
|
||||||
|
const msg_buf: []const u8 = blk: {
|
||||||
|
var frame_msg = protocol.protobuf.FrameMessage.init(self.allocator);
|
||||||
|
defer frame_msg.deinit();
|
||||||
|
frame_msg.display = update.display.toProtobuf(self.allocator);
|
||||||
|
frame_msg.pixels = .{ .Const = update.frame };
|
||||||
|
|
||||||
|
var msg = protocol.protobuf.ServerMessage.init(self.allocator);
|
||||||
|
defer msg.deinit();
|
||||||
|
msg.frame = frame_msg;
|
||||||
|
break :blk try msg.encode(self.allocator);
|
||||||
|
};
|
||||||
|
defer self.allocator.free(msg_buf);
|
||||||
|
|
||||||
|
const channel_id_str = try std.fmt.allocPrint(self.allocator, "{d}", .{update.channel_id});
|
||||||
|
defer self.allocator.free(channel_id_str);
|
||||||
|
|
||||||
|
WebsocketHandler.publish(.{
|
||||||
|
.channel = channel_id_str,
|
||||||
|
.message = msg_buf,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
src/eink_feed_server/dispatchers/root.zig
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub const FrameUpdate = @import("FrameUpdate.zig");
|
64
src/eink_feed_server/main.zig
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const clap = @import("clap");
|
||||||
|
|
||||||
|
const server = @import("eink_feed_server");
|
||||||
|
|
||||||
|
pub const ConfigFile = struct {
|
||||||
|
channels: []server.web.api.models.Channel,
|
||||||
|
clients: []server.Config.Client,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{
|
||||||
|
.thread_safe = true,
|
||||||
|
}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
{
|
||||||
|
const params = comptime clap.parseParamsComptime(
|
||||||
|
\\-h, --help Display this help and exit.
|
||||||
|
\\--port <u16> Web server port.
|
||||||
|
\\--config <str> Path to config file.
|
||||||
|
);
|
||||||
|
|
||||||
|
var diag = clap.Diagnostic{};
|
||||||
|
var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{
|
||||||
|
.diagnostic = &diag,
|
||||||
|
.allocator = allocator,
|
||||||
|
}) catch |err| {
|
||||||
|
diag.report(std.io.getStdErr().writer(), err) catch {};
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
defer res.deinit();
|
||||||
|
|
||||||
|
if (res.args.help != 0 or
|
||||||
|
res.args.port == null or
|
||||||
|
res.args.config == null)
|
||||||
|
{
|
||||||
|
const writer = std.io.getStdErr().writer();
|
||||||
|
try writer.writeAll("eink-feed-server\n");
|
||||||
|
return clap.help(writer, clap.Help, ¶ms, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
var config_file: std.json.Parsed(ConfigFile) = undefined;
|
||||||
|
{
|
||||||
|
const data = try std.fs.cwd().readFileAlloc(allocator, res.args.config.?, 1 * 1024 * 1024);
|
||||||
|
defer allocator.free(data);
|
||||||
|
config_file = try std.json.parseFromSlice(
|
||||||
|
ConfigFile,
|
||||||
|
allocator,
|
||||||
|
data,
|
||||||
|
.{ .allocate = .alloc_always, .ignore_unknown_fields = true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
defer config_file.deinit();
|
||||||
|
|
||||||
|
const config: server.Config = .{
|
||||||
|
.web_port = res.args.port.?,
|
||||||
|
.channels = config_file.value.channels,
|
||||||
|
.clients = config_file.value.clients,
|
||||||
|
};
|
||||||
|
try server.run(allocator, &config);
|
||||||
|
}
|
||||||
|
}
|
7
src/eink_feed_server/models/FrameUpdateMsg.zig
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
const protocol = @import("eink_feed_protocol");
|
||||||
|
|
||||||
|
const models = @import("root.zig");
|
||||||
|
|
||||||
|
channel_id: models.Channel.Id,
|
||||||
|
display: protocol.models.Display,
|
||||||
|
frame: []const u8,
|
36
src/eink_feed_server/models/root.zig
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const protocol = @import("eink_feed_protocol");
|
||||||
|
|
||||||
|
pub const FrameUpdateMsg = @import("FrameUpdateMsg.zig");
|
||||||
|
|
||||||
|
// pub const ChannelFrame = struct {
|
||||||
|
// timestamp: u64,
|
||||||
|
// frame: []const u8,
|
||||||
|
// };
|
||||||
|
|
||||||
|
pub const Channel = struct {
|
||||||
|
pub const Id = u32;
|
||||||
|
pub const NameMaxLen = 16;
|
||||||
|
|
||||||
|
id: Id,
|
||||||
|
name: [NameMaxLen]u8,
|
||||||
|
name_len: usize = 0,
|
||||||
|
display: protocol.models.Display,
|
||||||
|
|
||||||
|
pub fn nameSlice(self: *const Channel) []const u8 {
|
||||||
|
return self.name[0..self.name_len];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DisplayClient = struct {
|
||||||
|
pub const NameMaxLen: usize = 16;
|
||||||
|
|
||||||
|
name: [NameMaxLen]u8,
|
||||||
|
name_len: usize = 0,
|
||||||
|
channel_id: Channel.Id,
|
||||||
|
|
||||||
|
pub fn nameSlice(self: *const DisplayClient) []const u8 {
|
||||||
|
return self.name[0..self.name_len];
|
||||||
|
}
|
||||||
|
};
|
120
src/eink_feed_server/mpsc.zig
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Generic slot with value type `T` for a ring buffer
|
||||||
|
pub fn Slot(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
/// Value
|
||||||
|
value: T,
|
||||||
|
|
||||||
|
/// Slot version
|
||||||
|
version: std.atomic.Value(usize),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ring buffer with value type `T` on a slice of slots
|
||||||
|
pub fn RingBuffer(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Ring buffer capacity
|
||||||
|
capacity: usize,
|
||||||
|
|
||||||
|
/// Buffer slice
|
||||||
|
buffer: []Slot(T),
|
||||||
|
|
||||||
|
/// Atomic head index of buffer (write index)
|
||||||
|
head: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
|
||||||
|
|
||||||
|
/// Tail index of buffer (read index)
|
||||||
|
tail: usize = 0,
|
||||||
|
|
||||||
|
/// Initializes ring buffer with a given buffer, throws `error.CapacityTooSmall` if the buffer capacity is less or equal than 1.
|
||||||
|
pub fn init(buffer: []Slot(T)) error{CapacityTooSmall}!Self {
|
||||||
|
if (buffer.len <= 1) {
|
||||||
|
return error.CapacityTooSmall;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (buffer, 0..) |*slot, i| {
|
||||||
|
slot.version = std.atomic.Value(usize).init(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.capacity = buffer.len,
|
||||||
|
.buffer = buffer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enqueues element. Throws `error.Overflow` if no more elements fit.
|
||||||
|
pub fn enqueue(self: *Self, value: T) error{Overflow}!void {
|
||||||
|
while (true) {
|
||||||
|
// Acquire write slot
|
||||||
|
const head = self.head.load(.acquire);
|
||||||
|
const index = head % self.capacity;
|
||||||
|
const slot = &self.buffer[index];
|
||||||
|
|
||||||
|
// Check if slot has been read (empty)
|
||||||
|
const expected_version = head;
|
||||||
|
if (slot.version.load(.acquire) != expected_version) {
|
||||||
|
return error.Overflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare and swap head index
|
||||||
|
if (self.head.cmpxchgStrong(
|
||||||
|
head,
|
||||||
|
head + 1,
|
||||||
|
.seq_cst,
|
||||||
|
.seq_cst,
|
||||||
|
)) |_| {
|
||||||
|
std.atomic.spinLoopHint(); // Retry again next cycle
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slot versioning
|
||||||
|
slot.value = value;
|
||||||
|
slot.version.store(expected_version + 1, .release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dequeues element. Throws `error.Underflow` if ring buffer is empty.
|
||||||
|
pub fn dequeue(self: *Self) error{Underflow}!T {
|
||||||
|
// Acquire read slot
|
||||||
|
const tail = self.tail;
|
||||||
|
const index = tail % self.capacity;
|
||||||
|
const slot = &self.buffer[index];
|
||||||
|
|
||||||
|
// Check is slot has been written to (full)
|
||||||
|
const expected_version = tail + 1;
|
||||||
|
if (slot.version.load(.acquire) != expected_version) {
|
||||||
|
return error.Underflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slot versioning
|
||||||
|
const value = slot.value;
|
||||||
|
slot.version.store(tail +% self.capacity, .release);
|
||||||
|
self.tail +%= 1;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dequeues all stores elements into a buffer. It must at least have a size of the buffer capacity.
|
||||||
|
pub fn dequeueAll(self: *Self, buf: []T) ![]T {
|
||||||
|
if (buf.len < self.capacity) {
|
||||||
|
return error.BufferTooSmall;
|
||||||
|
}
|
||||||
|
|
||||||
|
const head = self.head.load(.acquire) % self.capacity;
|
||||||
|
var tail = self.tail % self.capacity;
|
||||||
|
var i: usize = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const val = self.dequeue() catch break;
|
||||||
|
buf[i] = val;
|
||||||
|
i += 1;
|
||||||
|
tail = (tail + 1) % self.capacity;
|
||||||
|
if (tail == head) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[0..i];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
51
src/eink_feed_server/msg_queue/queue.zig
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const mpsc = @import("../mpsc.zig");
|
||||||
|
|
||||||
|
pub fn Queue(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
ring_buffer: mpsc.RingBuffer(T),
|
||||||
|
|
||||||
|
pub fn init(buffer: []mpsc.Slot(T)) !Self {
|
||||||
|
return .{
|
||||||
|
.ring_buffer = try mpsc.RingBuffer(T).init(buffer),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capacity(self: *Self) usize {
|
||||||
|
return self.ring_buffer.capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryEnqueue(self: *Self, value: T) !void {
|
||||||
|
try self.ring_buffer.enqueue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enqueue(self: *Self, value: T) void {
|
||||||
|
while (true) {
|
||||||
|
self.ring_buffer.enqueue(value) catch |err| switch (err) {
|
||||||
|
error.Overflow => {
|
||||||
|
std.atomic.spinLoopHint();
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryDequeue(self: *Self) !T {
|
||||||
|
return try self.ring_buffer.dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dequeue(self: *Self) ?T {
|
||||||
|
return self.ring_buffer.dequeue() catch |err| switch (err) {
|
||||||
|
error.Underflow => return null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryDequeueAll(self: *Self, buf: []T) ![]T {
|
||||||
|
return try self.ring_buffer.dequeueAll(buf);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
118
src/eink_feed_server/msg_queue/root.zig
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const mpsc = @import("../mpsc.zig");
|
||||||
|
const queue = @import("queue.zig");
|
||||||
|
|
||||||
|
pub fn MsgQueueUnmanaged(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
queue: queue.Queue(T),
|
||||||
|
// consumer_mutex: std.Thread.Mutex = std.Thread.Mutex{},
|
||||||
|
cond: std.Thread.Condition = std.Thread.Condition{},
|
||||||
|
|
||||||
|
pub fn init(buffer: []mpsc.Slot(T)) !Self {
|
||||||
|
return .{
|
||||||
|
.queue = try queue.Queue(T).init(buffer),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capacity(self: *Self) usize {
|
||||||
|
return self.queue.capacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryEnqueue(self: *Self, value: T) !void {
|
||||||
|
try self.queue.tryEnqueue(value);
|
||||||
|
self.cond.broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enqueue(self: *Self, value: T) void {
|
||||||
|
self.queue.enqueue(value);
|
||||||
|
self.cond.broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryDequeue(self: *Self) !T {
|
||||||
|
return try self.queue.tryDequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dequeue(self: *Self) T {
|
||||||
|
var m = std.Thread.Mutex{};
|
||||||
|
m.lock();
|
||||||
|
defer m.unlock();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (self.queue.dequeue()) |val| return val;
|
||||||
|
self.cond.wait(&m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryDequeueAll(self: *Self, buf: []T) ![]T {
|
||||||
|
return try self.queue.tryDequeueAll(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dequeueAll(self: *Self, buf: []T) ![]T {
|
||||||
|
var m = std.Thread.Mutex{};
|
||||||
|
m.lock();
|
||||||
|
defer m.unlock();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const slice = try self.queue.tryDequeueAll(buf);
|
||||||
|
if (slice.len > 0) return slice;
|
||||||
|
self.cond.wait(&m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn MsgQueueManaged(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
buffer: []mpsc.Slot(T),
|
||||||
|
msg_queue: MsgQueueUnmanaged(T),
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, cap: usize) !Self {
|
||||||
|
const buffer = try allocator.alloc(mpsc.Slot(T), cap);
|
||||||
|
errdefer allocator.free(buffer);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.buffer = buffer,
|
||||||
|
.msg_queue = try MsgQueueUnmanaged(T).init(buffer),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmanaged(self: *Self) *MsgQueueUnmanaged(T) {
|
||||||
|
return &self.msg_queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capacity(self: *Self) usize {
|
||||||
|
return self.msg_queue.capacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryEnqueue(self: *Self, value: T) !void {
|
||||||
|
try self.msg_queue.tryEnqueue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enqueue(self: *Self, value: T) void {
|
||||||
|
self.msg_queue.enqueue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryDequeue(self: *Self) !T {
|
||||||
|
return try self.msg_queue.tryDequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dequeue(self: *Self) T {
|
||||||
|
return self.msg_queue.dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryDequeueAll(self: *Self, buf: []T) ![]T {
|
||||||
|
return try self.msg_queue.tryDequeueAll(buf);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
16
src/eink_feed_server/result.zig
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn ResultArena(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
arena: *std.heap.ArenaAllocator,
|
||||||
|
value: T,
|
||||||
|
|
||||||
|
pub fn deinit(self: Self) void {
|
||||||
|
const allocator = self.arena.child_allocator;
|
||||||
|
self.arena.deinit();
|
||||||
|
allocator.destroy(self.arena);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
85
src/eink_feed_server/root.zig
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const mpsc = @import("mpsc.zig");
|
||||||
|
pub const msg_queue = @import("msg_queue/root.zig");
|
||||||
|
pub const dispatchers = @import("dispatchers/root.zig");
|
||||||
|
|
||||||
|
pub const ResultArena = @import("result.zig").ResultArena;
|
||||||
|
|
||||||
|
pub const models = @import("models/root.zig");
|
||||||
|
pub const web = @import("web/root.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.eink_feed_server);
|
||||||
|
|
||||||
|
pub const Config = struct {
|
||||||
|
pub const Client = struct {
|
||||||
|
name: []const u8,
|
||||||
|
channel_id: models.Channel.Id,
|
||||||
|
};
|
||||||
|
|
||||||
|
web_port: u16,
|
||||||
|
channels: []web.api.models.Channel,
|
||||||
|
clients: []Client,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ConfigError = error{
|
||||||
|
InvalidChannelName,
|
||||||
|
InvalidClientName,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn dispatcherWrapper(dispatcher: *dispatchers.FrameUpdate) void {
|
||||||
|
dispatcher.start() catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(allocator: std.mem.Allocator, config: *const Config) !void {
|
||||||
|
const capacity: usize = 1024;
|
||||||
|
var queue = try msg_queue.MsgQueueManaged(ResultArena(models.FrameUpdateMsg))
|
||||||
|
.init(allocator, capacity);
|
||||||
|
defer queue.deinit();
|
||||||
|
|
||||||
|
var channels_store = web.stores.Channels.init(allocator, queue.unmanaged());
|
||||||
|
defer channels_store.deinit();
|
||||||
|
|
||||||
|
for (config.channels) |channel| {
|
||||||
|
if (channel.name.len > models.Channel.NameMaxLen)
|
||||||
|
return ConfigError.InvalidChannelName;
|
||||||
|
|
||||||
|
var name: [models.Channel.NameMaxLen]u8 = undefined;
|
||||||
|
std.mem.copyBackwards(u8, &name, channel.name);
|
||||||
|
|
||||||
|
_ = try channels_store.add(&.{
|
||||||
|
.id = channel.id,
|
||||||
|
.name = name,
|
||||||
|
.name_len = channel.name.len,
|
||||||
|
.display = .{
|
||||||
|
.width = channel.display.width,
|
||||||
|
.height = channel.display.height,
|
||||||
|
.orientation = channel.display.orientation.toInternal(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (config.clients) |client| {
|
||||||
|
if (client.name.len > models.DisplayClient.NameMaxLen)
|
||||||
|
return ConfigError.InvalidClientName;
|
||||||
|
|
||||||
|
var name: [models.DisplayClient.NameMaxLen]u8 = undefined;
|
||||||
|
std.mem.copyBackwards(u8, &name, client.name);
|
||||||
|
|
||||||
|
try channels_store.clients.add(&.{
|
||||||
|
.name = name,
|
||||||
|
.name_len = client.name.len,
|
||||||
|
.channel_id = client.channel_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var dispatcher = dispatchers.FrameUpdate.init(
|
||||||
|
allocator,
|
||||||
|
&.{ .capacity = capacity },
|
||||||
|
queue.unmanaged(),
|
||||||
|
);
|
||||||
|
log.info("Starting dispatcher", .{});
|
||||||
|
_ = try std.Thread.spawn(.{}, dispatcherWrapper, .{&dispatcher});
|
||||||
|
|
||||||
|
try web.start(allocator, config.web_port, &channels_store);
|
||||||
|
}
|
97
src/eink_feed_server/web/api/models/root.zig
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const protocol = @import("eink_feed_protocol");
|
||||||
|
|
||||||
|
const models = @import("../../../models/root.zig");
|
||||||
|
|
||||||
|
// pub const PixelFormat = enum {
|
||||||
|
// grayscale,
|
||||||
|
|
||||||
|
// pub fn fromInternal(value: protocol.models.PixelFormat) PixelFormat {
|
||||||
|
// return switch (value) {
|
||||||
|
// .grayscale => .grayscale,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
pub const Orientation = enum {
|
||||||
|
portrait_up,
|
||||||
|
portrait_down,
|
||||||
|
landscape_left,
|
||||||
|
landscape_right,
|
||||||
|
|
||||||
|
pub fn fromString(str: []const u8) ?Orientation {
|
||||||
|
if (std.mem.eql(u8, str, "portrait_up")) {
|
||||||
|
return .portrait_up;
|
||||||
|
} else if (std.mem.eql(u8, str, "portrait_down")) {
|
||||||
|
return .portrait_down;
|
||||||
|
} else if (std.mem.eql(u8, str, "landscape_left")) {
|
||||||
|
return .landscape_left;
|
||||||
|
} else if (std.mem.eql(u8, str, "landscape_right")) {
|
||||||
|
return .landscape_right;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: Orientation) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.portrait_up => "portrait_up",
|
||||||
|
.portrait_down => "portrait_down",
|
||||||
|
.landscape_left => "landscape_left",
|
||||||
|
.landscape_right => "landscape_right",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fromInternal(value: protocol.models.Orientation) Orientation {
|
||||||
|
return switch (value) {
|
||||||
|
.portrait_up => .portrait_up,
|
||||||
|
.portrait_down => .portrait_down,
|
||||||
|
.landscape_left => .landscape_left,
|
||||||
|
.landscape_right => .landscape_right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toInternal(value: Orientation) protocol.models.Orientation {
|
||||||
|
return switch (value) {
|
||||||
|
.portrait_up => .portrait_up,
|
||||||
|
.portrait_down => .portrait_down,
|
||||||
|
.landscape_left => .landscape_left,
|
||||||
|
.landscape_right => .landscape_right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Display = struct {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
orientation: Orientation,
|
||||||
|
|
||||||
|
pub fn fromInternal(value: protocol.models.Display) Display {
|
||||||
|
return .{
|
||||||
|
.width = value.width,
|
||||||
|
.height = value.height,
|
||||||
|
.orientation = Orientation.fromInternal(value.orientation),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ChannelFrame = struct {
|
||||||
|
timestamp: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Channel = struct {
|
||||||
|
pub const Id = u32;
|
||||||
|
|
||||||
|
id: Id,
|
||||||
|
name: []const u8,
|
||||||
|
display: Display,
|
||||||
|
|
||||||
|
pub fn fromInternal(value: *const models.Channel) Channel {
|
||||||
|
return Channel{
|
||||||
|
.id = value.id,
|
||||||
|
.name = value.nameSlice(),
|
||||||
|
.display = Display.fromInternal(value.display),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
1
src/eink_feed_server/web/api/root.zig
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub const models = @import("models/root.zig");
|
96
src/eink_feed_server/web/endpoints/Channel.zig
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zap = @import("zap");
|
||||||
|
|
||||||
|
const web = @import("../root.zig");
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
channels: *web.stores.Channels,
|
||||||
|
|
||||||
|
path: []const u8,
|
||||||
|
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_console,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, channels: *web.stores.Channels, path: []const u8) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.channels = channels,
|
||||||
|
.path = path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn channelIdFromParams(r: zap.Request) ?web.api.models.Channel.Id {
|
||||||
|
if (r.getParamSlice("id")) |str| {
|
||||||
|
return std.fmt.parseUnsigned(web.api.models.Channel.Id, str, 10) catch null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parseChannelId(r: zap.Request) ?web.api.models.Channel.Id {
|
||||||
|
if (channelIdFromParams(r)) |id| return id;
|
||||||
|
r.setStatus(.bad_request);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *Self, r: zap.Request) !void {
|
||||||
|
web.endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.parseQuery();
|
||||||
|
const channel_id = parseChannelId(r) orelse return;
|
||||||
|
const channel_internal = self.channels.get(channel_id) orelse {
|
||||||
|
r.setStatus(.no_content);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const channel = web.api.models.Channel.fromInternal(&channel_internal);
|
||||||
|
const json = try std.json.stringifyAlloc(self.allocator, channel, .{});
|
||||||
|
defer self.allocator.free(json);
|
||||||
|
try r.setContentType(.JSON);
|
||||||
|
try r.sendBody(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn post(self: *Self, r: zap.Request) !void {
|
||||||
|
web.endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.setStatus(.method_not_allowed);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put(self: *Self, r: zap.Request) !void {
|
||||||
|
web.endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.setStatus(.method_not_allowed);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(self: *Self, r: zap.Request) !void {
|
||||||
|
web.endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.setStatus(.method_not_allowed);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patch(self: *Self, r: zap.Request) !void {
|
||||||
|
web.endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.setStatus(.method_not_allowed);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn options(self: *Self, r: zap.Request) !void {
|
||||||
|
web.endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
try r.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
try r.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS, HEAD");
|
||||||
|
r.setStatus(.no_content);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn head(self: *Self, r: zap.Request) !void {
|
||||||
|
web.endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.setStatus(.no_content);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
236
src/eink_feed_server/web/endpoints/ChannelFrame.zig
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zap = @import("zap");
|
||||||
|
const zigimg = @import("zigimg");
|
||||||
|
|
||||||
|
const msg_queue = @import("../../msg_queue/root.zig");
|
||||||
|
const ResultArena = @import("../../result.zig").ResultArena;
|
||||||
|
const models = @import("../../models/root.zig");
|
||||||
|
const endpoints = @import("root.zig");
|
||||||
|
const stores = @import("../stores/root.zig");
|
||||||
|
const web = @import("../root.zig");
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
channels: *stores.Channels,
|
||||||
|
|
||||||
|
path: []const u8,
|
||||||
|
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_console,
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
channels: *stores.Channels,
|
||||||
|
path: []const u8,
|
||||||
|
) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.channels = channels,
|
||||||
|
.path = path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *Self, r: zap.Request) !void {
|
||||||
|
endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.setStatus(.method_not_allowed);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
|
||||||
|
// endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
// r.parseQuery();
|
||||||
|
// const id = parseChannelId(r) orelse return;
|
||||||
|
// const channel_internal = self.channels.get(id) orelse {
|
||||||
|
// r.setStatus(.no_content);
|
||||||
|
// r.markAsFinished(true);
|
||||||
|
// return;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const channel = server.web.api.models.Channel.fromInternal(channel_internal);
|
||||||
|
// const json = try std.json.stringifyAlloc(self.allocator, channel, .{});
|
||||||
|
// defer self.allocator.free(json);
|
||||||
|
// try r.setContentType(.JSON);
|
||||||
|
// try r.sendBody(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn orientationFromParams(r: zap.Request) ?web.api.models.Orientation {
|
||||||
|
if (r.getParamSlice("orientation")) |str| {
|
||||||
|
return web.api.models.Orientation.fromString(str);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseOrientation(r: zap.Request) ?web.api.models.Orientation {
|
||||||
|
if (orientationFromParams(r)) |val| return val;
|
||||||
|
r.setStatus(.bad_request);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn post(self: *Self, r: zap.Request) !void {
|
||||||
|
endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
const channel_id = endpoints.Channel.parseChannelId(r) orelse return;
|
||||||
|
const orientation = parseOrientation(r) orelse return;
|
||||||
|
const channel = self.channels.get(channel_id) orelse {
|
||||||
|
r.setStatus(.no_content);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
try r.parseBody();
|
||||||
|
const params = try r.parametersToOwnedList(self.allocator);
|
||||||
|
defer params.deinit();
|
||||||
|
|
||||||
|
var file_mimetype: ?[]const u8 = null;
|
||||||
|
var file_data: ?[]const u8 = null;
|
||||||
|
blk: {
|
||||||
|
for (params.items) |kv| {
|
||||||
|
if (kv.value) |v| {
|
||||||
|
switch (v) {
|
||||||
|
.Hash_Binfile => |*file| {
|
||||||
|
if (file.mimetype == null or file.data == null) {
|
||||||
|
r.setStatus(.bad_request);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_mimetype = file.mimetype.?;
|
||||||
|
file_data = file.data.?;
|
||||||
|
break :blk;
|
||||||
|
},
|
||||||
|
.Array_Binfile => {
|
||||||
|
r.setStatus(.bad_request);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var image: zigimg.ImageUnmanaged = undefined;
|
||||||
|
if (file_mimetype != null and file_data != null) {
|
||||||
|
if (std.mem.eql(u8, file_mimetype.?, "image/bmp")) {
|
||||||
|
var stream_source = std.io.StreamSource{
|
||||||
|
.const_buffer = std.io.fixedBufferStream(file_data.?),
|
||||||
|
};
|
||||||
|
|
||||||
|
image = zigimg.formats.bmp.BMP.readImage(self.allocator, &stream_source) catch |err| switch (err) {
|
||||||
|
error.OutOfMemory => |e| return e,
|
||||||
|
else => {
|
||||||
|
r.setStatus(.unsupported_media_type);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
errdefer image.deinit(self.allocator);
|
||||||
|
|
||||||
|
image.convert(self.allocator, .grayscale8) catch |err| switch (err) {
|
||||||
|
error.OutOfMemory => |e| return e,
|
||||||
|
else => {
|
||||||
|
r.setStatus(.internal_server_error);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (std.mem.eql(u8, file_mimetype.?, "image/png")) {
|
||||||
|
var stream_source = std.io.StreamSource{
|
||||||
|
.const_buffer = std.io.fixedBufferStream(file_data.?),
|
||||||
|
};
|
||||||
|
|
||||||
|
image = zigimg.formats.png.PNG.readImage(self.allocator, &stream_source) catch |err| switch (err) {
|
||||||
|
error.OutOfMemory => |e| return e,
|
||||||
|
else => {
|
||||||
|
r.setStatus(.unsupported_media_type);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
errdefer image.deinit(self.allocator);
|
||||||
|
|
||||||
|
image.convert(self.allocator, .grayscale8) catch |err| switch (err) {
|
||||||
|
error.OutOfMemory => |e| return e,
|
||||||
|
else => {
|
||||||
|
r.setStatus(.internal_server_error);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
r.setStatus(.unsupported_media_type);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.setStatus(.unsupported_media_type);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
defer image.deinit(self.allocator);
|
||||||
|
|
||||||
|
if (image.width > std.math.maxInt(u32) or image.height > std.math.maxInt(u32)) {
|
||||||
|
r.setStatus(.unsupported_media_type);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = ResultArena(models.FrameUpdateMsg){
|
||||||
|
.arena = try self.allocator.create(std.heap.ArenaAllocator),
|
||||||
|
.value = undefined,
|
||||||
|
};
|
||||||
|
errdefer self.allocator.destroy(msg.arena);
|
||||||
|
msg.arena.* = std.heap.ArenaAllocator.init(self.allocator);
|
||||||
|
errdefer msg.arena.deinit();
|
||||||
|
|
||||||
|
msg.value = .{
|
||||||
|
.channel_id = channel.id,
|
||||||
|
.display = .{
|
||||||
|
.width = @intCast(image.width),
|
||||||
|
.height = @intCast(image.height),
|
||||||
|
.orientation = orientation.toInternal(),
|
||||||
|
},
|
||||||
|
.frame = try msg.arena.allocator().dupe(u8, image.rawBytes()),
|
||||||
|
};
|
||||||
|
self.channels.queue.enqueue(msg);
|
||||||
|
|
||||||
|
r.setStatus(.created);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put(self: *Self, r: zap.Request) !void {
|
||||||
|
endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.setStatus(.method_not_allowed);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(self: *Self, r: zap.Request) !void {
|
||||||
|
endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.setStatus(.method_not_allowed);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patch(self: *Self, r: zap.Request) !void {
|
||||||
|
endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.setStatus(.method_not_allowed);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn options(self: *Self, r: zap.Request) !void {
|
||||||
|
endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
try r.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
try r.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS, HEAD");
|
||||||
|
r.setStatus(.no_content);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn head(self: *Self, r: zap.Request) !void {
|
||||||
|
endpoints.pathGuard(r, self.path) catch return;
|
||||||
|
|
||||||
|
r.setStatus(.no_content);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
}
|
31
src/eink_feed_server/web/endpoints/root.zig
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zap = @import("zap");
|
||||||
|
|
||||||
|
pub const ws = @import("ws/root.zig");
|
||||||
|
|
||||||
|
pub const Channel = @import("Channel.zig");
|
||||||
|
pub const ChannelFrame = @import("ChannelFrame.zig");
|
||||||
|
|
||||||
|
pub fn pathGuard(r: zap.Request, endpoint_path: []const u8) error{NotFound}!void {
|
||||||
|
if (r.path) |path| {
|
||||||
|
if (!std.mem.eql(u8, path, endpoint_path)) {
|
||||||
|
if (path.len >= endpoint_path.len and std.mem.eql(u8, path[0..endpoint_path.len], endpoint_path)) {
|
||||||
|
for (1..(path.len - endpoint_path.len + 1)) |i| {
|
||||||
|
if (path[endpoint_path.len - 1 + i] != '/') {
|
||||||
|
r.setStatus(.not_found);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return error.NotFound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.setStatus(.not_found);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return error.NotFound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.setStatus(.not_found);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return error.NotFound;
|
||||||
|
}
|
||||||
|
}
|
187
src/eink_feed_server/web/endpoints/ws/Display.zig
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zap = @import("zap");
|
||||||
|
const WebSockets = zap.WebSockets;
|
||||||
|
|
||||||
|
const protocol = @import("eink_feed_protocol");
|
||||||
|
|
||||||
|
const models = @import("../../../models/root.zig");
|
||||||
|
const web = @import("../../root.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.ws_display);
|
||||||
|
|
||||||
|
var global_context_manager: ContextManager = undefined;
|
||||||
|
|
||||||
|
const Context = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
channel: []const u8,
|
||||||
|
client: models.DisplayClient,
|
||||||
|
|
||||||
|
channels: *web.stores.Channels,
|
||||||
|
|
||||||
|
subscribe_args: WebsocketHandler.SubscribeArgs,
|
||||||
|
settings: WebsocketHandler.WebSocketSettings,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, channels: *web.stores.Channels) void {
|
||||||
|
global_context_manager = ContextManager.init(allocator, channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit() void {
|
||||||
|
global_context_manager.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const WebsocketHandler = WebSockets.Handler(Context);
|
||||||
|
|
||||||
|
const ContextList = std.ArrayList(*Context);
|
||||||
|
|
||||||
|
pub const ContextManager = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
new_context_lock: std.Thread.Mutex = .{},
|
||||||
|
contexts: ContextList = undefined,
|
||||||
|
channels: *web.stores.Channels,
|
||||||
|
|
||||||
|
fn init(allocator: std.mem.Allocator, channels: *web.stores.Channels) @This() {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.contexts = ContextList.init(allocator),
|
||||||
|
.channels = channels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *@This()) void {
|
||||||
|
// for (self.contexts.items) |ctx| {
|
||||||
|
// }
|
||||||
|
self.contexts.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn newContext(self: *@This(), channel: []const u8, client: models.DisplayClient) !*Context {
|
||||||
|
self.new_context_lock.lock();
|
||||||
|
defer self.new_context_lock.unlock();
|
||||||
|
|
||||||
|
const ctx = try self.allocator.create(Context);
|
||||||
|
errdefer self.allocator.destroy(ctx);
|
||||||
|
|
||||||
|
ctx.* = .{
|
||||||
|
.allocator = self.allocator,
|
||||||
|
.channel = channel,
|
||||||
|
.client = client,
|
||||||
|
.channels = self.channels,
|
||||||
|
.subscribe_args = .{
|
||||||
|
.channel = channel,
|
||||||
|
.context = ctx,
|
||||||
|
},
|
||||||
|
.settings = .{
|
||||||
|
.on_open = onOpenWebsocket,
|
||||||
|
.on_close = onCloseWebsocket,
|
||||||
|
.on_message = onMessageWebsocket,
|
||||||
|
.context = ctx,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn onOpenWebsocket(context: ?*Context, handle: WebSockets.WsHandle) !void {
|
||||||
|
if (context == null) return;
|
||||||
|
const ctx = context.?;
|
||||||
|
|
||||||
|
const channel = ctx.channels.get(ctx.client.channel_id) orelse {
|
||||||
|
log.err("Error getting channel: {}", .{ctx.client.channel_id});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
log.info("channel: {s}", .{channel.nameSlice()});
|
||||||
|
|
||||||
|
_ = WebsocketHandler.subscribe(handle, &ctx.subscribe_args) catch |err| {
|
||||||
|
log.err("Error opening websocket: {any}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
log.info("New websocket opened ({s})", .{ctx.channel});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onCloseWebsocket(context: ?*Context, uuid_ws: isize) !void {
|
||||||
|
_ = uuid_ws;
|
||||||
|
|
||||||
|
if (context) |ctx| {
|
||||||
|
log.info("Websocket closed ({s})", .{ctx.channel});
|
||||||
|
global_context_manager.allocator.free(ctx.channel);
|
||||||
|
global_context_manager.allocator.destroy(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onMessageWebsocket(
|
||||||
|
context: ?*Context,
|
||||||
|
handle: WebSockets.WsHandle,
|
||||||
|
message: []const u8,
|
||||||
|
is_text: bool,
|
||||||
|
) !void {
|
||||||
|
_ = handle;
|
||||||
|
_ = is_text;
|
||||||
|
|
||||||
|
if (context == null) return;
|
||||||
|
const ctx = context.?;
|
||||||
|
|
||||||
|
const msg = protocol.protobuf.ClientMessage.decode(message, ctx.allocator) catch |err| switch (err) {
|
||||||
|
error.OutOfMemory => |e| return e,
|
||||||
|
else => return,
|
||||||
|
};
|
||||||
|
defer msg.deinit();
|
||||||
|
|
||||||
|
if (msg.key_update) |ku| {
|
||||||
|
if (ku.key_update) |key_update| {
|
||||||
|
log.info("Key update ({s}): {s} => {}", .{ ctx.client.nameSlice(), key_update.key_name.getSlice(), key_update.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseClientName(allocator: std.mem.Allocator, r: zap.Request, len: *usize) !?[models.DisplayClient.NameMaxLen]u8 {
|
||||||
|
if (try r.getParamStr(allocator, "name")) |str| {
|
||||||
|
defer allocator.free(str);
|
||||||
|
if (str.len > models.DisplayClient.NameMaxLen) {
|
||||||
|
r.setStatus(.bad_request);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
len.* = str.len;
|
||||||
|
var name: [models.DisplayClient.NameMaxLen:0]u8 = undefined;
|
||||||
|
std.mem.copyBackwards(u8, &name, str);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
r.setStatus(.bad_request);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onUpgrade(r: zap.Request, target_protocol: []const u8) !void {
|
||||||
|
if (!std.mem.eql(u8, target_protocol, "websocket")) {
|
||||||
|
log.warn("Received illegal protocol: {s}", .{target_protocol});
|
||||||
|
r.setStatus(.bad_request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
r.parseQuery();
|
||||||
|
var name_len: usize = undefined;
|
||||||
|
const name = try parseClientName(global_context_manager.allocator, r, &name_len) orelse return;
|
||||||
|
log.info("client name: '{s}'", .{name[0..name_len]});
|
||||||
|
|
||||||
|
const client = global_context_manager.channels.clients.get(name[0..name_len]) orelse {
|
||||||
|
r.setStatus(.bad_request);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const channel = try std.fmt.allocPrint(global_context_manager.allocator, "{d}", .{client.channel_id});
|
||||||
|
var context = global_context_manager.newContext(channel, client) catch |err| {
|
||||||
|
log.err("Error creating context: {any}", .{err});
|
||||||
|
global_context_manager.allocator.free(channel);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
WebsocketHandler.upgrade(r.h, &context.settings) catch |err| {
|
||||||
|
log.err("Error during websocket upgrade: {any}", .{err});
|
||||||
|
global_context_manager.allocator.free(channel);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
30
src/eink_feed_server/web/endpoints/ws/root.zig
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zap = @import("zap");
|
||||||
|
|
||||||
|
const web = @import("../../root.zig");
|
||||||
|
|
||||||
|
pub const Display = @import("Display.zig");
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, channels: *web.stores.Channels) void {
|
||||||
|
Display.init(allocator, channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit() void {
|
||||||
|
Display.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onUpgrade(r: zap.Request, target_protocol: []const u8) !void {
|
||||||
|
if (r.path == null) {
|
||||||
|
r.setStatus(.not_found);
|
||||||
|
r.markAsFinished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
web.endpoints.pathGuard(r, "/api/ws/display") catch return;
|
||||||
|
try Display.onUpgrade(r, target_protocol);
|
||||||
|
|
||||||
|
// if (std.mem.eql(u8, path, "/display/ws")) {
|
||||||
|
// try Display.onUpgrade(r, target_protocol);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
}
|
48
src/eink_feed_server/web/root.zig
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zap = @import("zap");
|
||||||
|
|
||||||
|
const protocol = @import("eink_feed_protocol");
|
||||||
|
|
||||||
|
const server = @import("eink_feed_server");
|
||||||
|
|
||||||
|
pub const api = @import("api/root.zig");
|
||||||
|
pub const endpoints = @import("endpoints/root.zig");
|
||||||
|
pub const stores = @import("stores/root.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.web);
|
||||||
|
|
||||||
|
fn onRequest(r: zap.Request) !void {
|
||||||
|
r.setStatus(.not_found);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(allocator: std.mem.Allocator, port: u16, channels_store: *stores.Channels) !void {
|
||||||
|
endpoints.ws.init(allocator, channels_store);
|
||||||
|
defer endpoints.ws.deinit();
|
||||||
|
|
||||||
|
var listener = zap.Endpoint.Listener.init(
|
||||||
|
allocator,
|
||||||
|
.{
|
||||||
|
.port = port,
|
||||||
|
.on_request = onRequest,
|
||||||
|
.on_upgrade = endpoints.ws.onUpgrade,
|
||||||
|
.max_clients = 1024,
|
||||||
|
.max_body_size = 50 * 1024 * 1024,
|
||||||
|
.log = true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
defer listener.deinit();
|
||||||
|
|
||||||
|
var channel_endpoint = endpoints.Channel.init(allocator, channels_store, "/api/channel");
|
||||||
|
try listener.register(&channel_endpoint);
|
||||||
|
|
||||||
|
var channel_frame_endpoint = endpoints.ChannelFrame.init(allocator, channels_store, "/api/frame");
|
||||||
|
try listener.register(&channel_frame_endpoint);
|
||||||
|
|
||||||
|
try listener.listen();
|
||||||
|
log.info("Listening on 0.0.0.0:{d}", .{port});
|
||||||
|
|
||||||
|
zap.start(.{
|
||||||
|
.threads = 4,
|
||||||
|
.workers = 1,
|
||||||
|
});
|
||||||
|
}
|
68
src/eink_feed_server/web/stores/Channels/Clients.zig
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const models = @import("../../../models/root.zig");
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
client_map: std.StringHashMap(models.DisplayClient),
|
||||||
|
lock: std.Thread.RwLock = .{},
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.client_map = std.StringHashMap(models.DisplayClient).init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.client_map.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(self: *Self, client: *const models.DisplayClient) !void {
|
||||||
|
self.lock.lock();
|
||||||
|
defer self.lock.unlock();
|
||||||
|
|
||||||
|
const name = client.nameSlice();
|
||||||
|
|
||||||
|
if (self.client_map.getPtr(name)) |val| {
|
||||||
|
val.channel_id = client.channel_id;
|
||||||
|
} else {
|
||||||
|
try self.client_map.put(name, .{
|
||||||
|
.name = client.name,
|
||||||
|
.name_len = client.name_len,
|
||||||
|
.channel_id = client.channel_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(self: *Self, name: []const u8) bool {
|
||||||
|
self.lock.lockShared();
|
||||||
|
defer self.lock.unlockShared();
|
||||||
|
|
||||||
|
return self.client_map.remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(self: *Self, name: []const u8) !bool {
|
||||||
|
self.lock.lock();
|
||||||
|
defer self.lock.unlock();
|
||||||
|
|
||||||
|
return self.client_map.get(name) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *Self, name: []const u8) ?models.DisplayClient {
|
||||||
|
self.lock.lock();
|
||||||
|
defer self.lock.unlock();
|
||||||
|
|
||||||
|
return self.client_map.get(name) orelse return null;
|
||||||
|
|
||||||
|
// const copy: server.models.DisplayClient = .{
|
||||||
|
// .name = try allocator.dupe(u8, client.name),
|
||||||
|
// .channel_id = client.channel_id,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return server.ResultArena(server.models.DisplayClient){
|
||||||
|
// .arena = arena,
|
||||||
|
// .value = copy,
|
||||||
|
// };
|
||||||
|
}
|
96
src/eink_feed_server/web/stores/Channels/root.zig
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const ResultArena = @import("../../../result.zig").ResultArena;
|
||||||
|
const msg_queue = @import("../../../msg_queue//root.zig");
|
||||||
|
const models = @import("../../../models/root.zig");
|
||||||
|
|
||||||
|
const Clients = @import("Clients.zig");
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
arena: std.heap.ArenaAllocator,
|
||||||
|
channels: std.AutoHashMap(models.Channel.Id, *models.Channel),
|
||||||
|
lock: std.Thread.RwLock = .{},
|
||||||
|
|
||||||
|
clients: Clients,
|
||||||
|
|
||||||
|
queue: *msg_queue.MsgQueueUnmanaged(ResultArena(models.FrameUpdateMsg)),
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, queue: *msg_queue.MsgQueueUnmanaged(ResultArena(models.FrameUpdateMsg))) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||||
|
.channels = std.AutoHashMap(models.Channel.Id, *models.Channel).init(allocator),
|
||||||
|
.clients = Clients.init(allocator),
|
||||||
|
.queue = queue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
// var iter = self.channels.valueIterator();
|
||||||
|
// while (iter.next()) |ptr| {
|
||||||
|
// const channel = ptr.*;
|
||||||
|
// self.allocator.free(channel.name);
|
||||||
|
// self.allocator.free(channel.description);
|
||||||
|
// if (channel.frame) |frame| {
|
||||||
|
// self.allocator.free(frame.frame);
|
||||||
|
// }
|
||||||
|
// self.allocator.destroy(channel);
|
||||||
|
// }
|
||||||
|
|
||||||
|
self.clients.deinit();
|
||||||
|
self.channels.deinit();
|
||||||
|
self.arena.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(self: *Self, channel: *const models.Channel) !?models.Channel.Id {
|
||||||
|
self.lock.lock();
|
||||||
|
defer self.lock.unlock();
|
||||||
|
|
||||||
|
if (self.channels.contains(channel.id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allocator = self.arena.allocator();
|
||||||
|
|
||||||
|
const copy = try allocator.create(models.Channel);
|
||||||
|
copy.id = channel.id;
|
||||||
|
@memcpy(©.name, &channel.name);
|
||||||
|
copy.name_len = channel.name_len;
|
||||||
|
copy.display = channel.display;
|
||||||
|
|
||||||
|
try self.channels.put(channel.id, copy);
|
||||||
|
return channel.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *Self, id: models.Channel.Id) ?models.Channel {
|
||||||
|
self.lock.lockShared();
|
||||||
|
defer self.lock.unlockShared();
|
||||||
|
|
||||||
|
const channel = self.channels.get(id) orelse return null;
|
||||||
|
|
||||||
|
var copy: models.Channel = undefined;
|
||||||
|
copy.id = channel.id;
|
||||||
|
@memcpy(©.name, &channel.name);
|
||||||
|
copy.name_len = channel.name_len;
|
||||||
|
copy.display = channel.display;
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(self: *Self, id: models.Channel.Id) bool {
|
||||||
|
self.lock.lock();
|
||||||
|
defer self.lock.unlock();
|
||||||
|
|
||||||
|
if (self.channels.fetchRemove(id)) |kv| {
|
||||||
|
const allocator = self.arena.allocator();
|
||||||
|
const channel = kv.value;
|
||||||
|
if (channel.frame) |frame| {
|
||||||
|
allocator.free(frame.frame);
|
||||||
|
}
|
||||||
|
allocator.destroy(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
1
src/eink_feed_server/web/stores/root.zig
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub const Channels = @import("Channels/root.zig");
|