From 57c7ed4523510a82f3397bcfb8f0e29d7cf552c6 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Tue, 25 Jun 2024 12:22:46 +0200 Subject: [PATCH 01/26] Addig user management to ZeServer Secret server token and header needed for admin. Otherwise you have to know / guess the user uuid --- .gitignore | 4 +- .../de/berlindroid/zekompanion/server/Main.kt | 22 ++++ .../zekompanion/server/routers/UserRouter.kt | 123 ++++++++++++++++++ .../zekompanion/server/user/UserRepository.kt | 87 +++++++++++++ 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt create mode 100644 zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt diff --git a/.gitignore b/.gitignore index dc8e5e70..5693d901 100644 --- a/.gitignore +++ b/.gitignore @@ -559,4 +559,6 @@ obj/ # End of https://www.toptal.com/developers/gitignore/api/python,android,androidstudio,intellij+all,windows,linux,macos,sublimetext,java,kotlin,circuitpython /zeapp/app/release/output-metadata.json /zeapp/app/debug/output-metadata.json -/zeapp/versions.properties +i/zeapp/versions.properties + +*.db diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt index fd2ea587..28e4a565 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt @@ -5,6 +5,12 @@ package de.berlindroid.zekompanion.server import de.berlindroid.zekompanion.server.routers.imageBin import de.berlindroid.zekompanion.server.routers.imagePng import de.berlindroid.zekompanion.server.routers.index +import de.berlindroid.zekompanion.server.routers.adminCreateUser +import de.berlindroid.zekompanion.server.routers.adminDeleteUser +import de.berlindroid.zekompanion.server.routers.adminListUsers +import de.berlindroid.zekompanion.server.routers.getUser +import de.berlindroid.zekompanion.server.routers.updateUser +import de.berlindroid.zekompanion.server.user.UserRepository import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.engine.* @@ -20,6 +26,8 @@ private const val LOCAL_TLS_PORT = 8443 private const val SSL_PASSWORD_ENV = "SSL_CERTIFICATE_PASSWORD" private const val KEYSTORE_RESOURCE_FILE = "/tmp/keystore.jks" +private const val USER_DB_FILE = "/tmp/user.db" + fun main(args: Array) { val keyPassword = try { @@ -32,6 +40,8 @@ fun main(args: Array) { val serverPort = extractServerPort(args, keyStore) println("Serving on port $serverPort.") + val users = UserRepository.load() + embeddedServer( Tomcat, environment = applicationEngineEnvironment { @@ -45,10 +55,22 @@ fun main(args: Array) { routing { staticResources("/", "static") { index() + exclude { file -> + file.path.endsWith("db") + } } imageBin() imagePng() + + // Callable from ZeFlasher only? + adminCreateUser(users) + adminListUsers(users) + adminDeleteUser(users) + + // TODO: Check if callable from ZeBadge (no ssl) + updateUser(users) + getUser(users) } } }, diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt new file mode 100644 index 00000000..bebe64d8 --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt @@ -0,0 +1,123 @@ +package de.berlindroid.zekompanion.server.routers + +import de.berlindroid.zekompanion.server.user.User +import de.berlindroid.zekompanion.server.user.UserRepository +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.call +import io.ktor.server.request.header +import io.ktor.server.request.receiveNullable +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.routing.Route +import io.ktor.server.routing.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.put +import io.ktor.util.pipeline.PipelineContext + + +suspend fun PipelineContext.withParameter( + key: String, + block: suspend PipelineContext.(value: String) -> Unit, +) { + if (call.parameters.contains(key)) { + val value = call.parameters[key] + if (value != null) { + block(value) + } + } +} + +suspend fun PipelineContext.ifAuthorized(block: suspend PipelineContext.() -> Unit) { + val authHeader = call.request.header("ZeAuth") + val authEnv = System.getenv("ZESERVER_AUTH_TOKEN") + if (authEnv.isEmpty() || authHeader == null || authEnv != authHeader) { + call.respondText(status = HttpStatusCode.Forbidden, text = "Forbidden") + } else { + block() + } +} + +fun Route.adminCreateUser(users: UserRepository) = + post("/api/user/") { + runCatching { + ifAuthorized { + val newUser = call.receiveNullable() ?: throw IllegalArgumentException("No user payload found.") + val uuidAdded = users.createUser(newUser) + + if (uuidAdded != null) { + call.respond(status = HttpStatusCode.Created, users.getUser(uuidAdded)!!) + } else { + call.respondText("invalid", status = HttpStatusCode.Forbidden) + } + + } + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.adminListUsers(users: UserRepository) = + get("/api/user/") { + runCatching { + ifAuthorized { + call.respond(status = HttpStatusCode.OK, users.getUsers()) + } + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.adminDeleteUser(users: UserRepository) = + delete("/api/user/{UUID}") { + runCatching { + ifAuthorized { + withParameter("UUID") { uuid -> + call.respond(status = HttpStatusCode.OK, users.deleteUser(uuid)) + } + } + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.updateUser(users: UserRepository) = + put("/api/user/{UUID}") { + runCatching { + withParameter("UUID") { uuid -> + val newUser = call.receiveNullable() ?: throw IllegalArgumentException("No user payload found.") + val userUpdated = users.updateUser(newUser.copy(uuid = uuid)) + + if (userUpdated) { + call.respondText(text = "OK") + } else { + call.respondText("invalid", status = HttpStatusCode.NotAcceptable) + } + } + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}", status = HttpStatusCode.NotAcceptable) + } + } + +fun Route.getUser(users: UserRepository) = + get("/api/user/{UUID}") { + runCatching { + withParameter("UUID") { uuid -> + val user = users.getUser(uuid) + if (user != null) { + call.respond(status = HttpStatusCode.OK, user) + } else { + call.respondText(status = HttpStatusCode.NotFound, text = "Not Found.") + } + } + call.respondText(status = HttpStatusCode.UnprocessableEntity, text = "No UUID.") + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt new file mode 100644 index 00000000..216c3833 --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt @@ -0,0 +1,87 @@ +package de.berlindroid.zekompanion.server.user + +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File +import java.io.FileNotFoundException +import java.util.UUID + +private const val DB_FILENAME = "./user.db" + +@Serializable +data class User( + val name: String? = null, + val iconUrl: String? = null, + val uuid: String? = null, +) + +class UserRepository private constructor( + private val users: MutableList = mutableListOf(), +) { + companion object { + fun load(): UserRepository = try { + UserRepository( + users = Json.decodeFromString(File(DB_FILENAME).readText()), + ) + } catch (notFound: FileNotFoundException) { + UserRepository() + } + + fun save(repo: UserRepository) = File(DB_FILENAME).writer().use { + it.write(Json.encodeToString(repo.users)) + } + } + + fun createUser(user: User): String? { + val existingUser = users.find { it.uuid == user.uuid } + if (existingUser != null || user.uuid != null) { + return null + } + + val uuid = UUID.randomUUID().toString() + users.add(user.copy(uuid = uuid)) + + save(this) + + return uuid + } + + fun getUser(uuid: String): User? { + return users.find { it.uuid == uuid } + } + + fun getUsers(): List { + return users.toList() + } + + fun updateUser(newUser: User): Boolean { + val index = users.indexOfFirst { it.uuid == newUser.uuid } + if (index < 0) { + return false + } + + if (newUser.uuid == null) { + return false + } else { + users[index] = newUser + } + + save(this) + + return true + } + + fun deleteUser(uuid: String): Boolean { + val user = users.find { it.uuid == uuid } + if (user == null) { + return false + } + + users.remove(user) + + save(this) + + return true + } +} From 01ab0074ac6614a9aa772675f96443997fcc26c1 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Tue, 25 Jun 2024 13:05:31 +0200 Subject: [PATCH 02/26] Adding ZePass Everyone can see all posts, only registered users can add new posts. (Response contains a poster id, which we might need to obfuscate, slim down or shenangianify --- .../de/berlindroid/zekompanion/server/Main.kt | 7 +++ .../zekompanion/server/routers/Helper.kt | 31 ++++++++++++ .../zekompanion/server/routers/UserRouter.kt | 29 +---------- .../server/routers/ZePassRouter.kt | 43 ++++++++++++++++ .../server/zepass/ZePassRepository.kt | 50 +++++++++++++++++++ 5 files changed, 133 insertions(+), 27 deletions(-) create mode 100644 zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt create mode 100644 zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt create mode 100644 zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt index 28e4a565..26814a73 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt @@ -8,9 +8,12 @@ import de.berlindroid.zekompanion.server.routers.index import de.berlindroid.zekompanion.server.routers.adminCreateUser import de.berlindroid.zekompanion.server.routers.adminDeleteUser import de.berlindroid.zekompanion.server.routers.adminListUsers +import de.berlindroid.zekompanion.server.routers.getPosts import de.berlindroid.zekompanion.server.routers.getUser +import de.berlindroid.zekompanion.server.routers.postPost import de.berlindroid.zekompanion.server.routers.updateUser import de.berlindroid.zekompanion.server.user.UserRepository +import de.berlindroid.zekompanion.server.zepass.ZePassRepository import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.engine.* @@ -41,6 +44,7 @@ fun main(args: Array) { println("Serving on port $serverPort.") val users = UserRepository.load() + val zepass = ZePassRepository.load() embeddedServer( Tomcat, @@ -71,6 +75,9 @@ fun main(args: Array) { // TODO: Check if callable from ZeBadge (no ssl) updateUser(users) getUser(users) + + postPost(zepass, users) + getPosts(zepass) } } }, diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt new file mode 100644 index 00000000..c008c39b --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt @@ -0,0 +1,31 @@ +package de.berlindroid.zekompanion.server.routers + +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.call +import io.ktor.server.request.header +import io.ktor.server.response.respondText +import io.ktor.util.pipeline.PipelineContext + + +suspend fun PipelineContext.withParameter( + key: String, + block: suspend PipelineContext.(value: String) -> Unit, +) { + if (call.parameters.contains(key)) { + val value = call.parameters[key] + if (value != null) { + block(value) + } + } +} + +suspend fun PipelineContext.ifAuthorized(block: suspend PipelineContext.() -> Unit) { + val authHeader = call.request.header("ZeAuth") + val authEnv = System.getenv("ZESERVER_AUTH_TOKEN") + if (authEnv.isEmpty() || authHeader == null || authEnv != authHeader) { + call.respondText(status = HttpStatusCode.Forbidden, text = "Forbidden") + } else { + block() + } +} diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt index bebe64d8..9bceead3 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt @@ -3,9 +3,7 @@ package de.berlindroid.zekompanion.server.routers import de.berlindroid.zekompanion.server.user.User import de.berlindroid.zekompanion.server.user.UserRepository import io.ktor.http.HttpStatusCode -import io.ktor.server.application.ApplicationCall import io.ktor.server.application.call -import io.ktor.server.request.header import io.ktor.server.request.receiveNullable import io.ktor.server.response.respond import io.ktor.server.response.respondText @@ -14,33 +12,10 @@ import io.ktor.server.routing.delete import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.put -import io.ktor.util.pipeline.PipelineContext -suspend fun PipelineContext.withParameter( - key: String, - block: suspend PipelineContext.(value: String) -> Unit, -) { - if (call.parameters.contains(key)) { - val value = call.parameters[key] - if (value != null) { - block(value) - } - } -} - -suspend fun PipelineContext.ifAuthorized(block: suspend PipelineContext.() -> Unit) { - val authHeader = call.request.header("ZeAuth") - val authEnv = System.getenv("ZESERVER_AUTH_TOKEN") - if (authEnv.isEmpty() || authHeader == null || authEnv != authHeader) { - call.respondText(status = HttpStatusCode.Forbidden, text = "Forbidden") - } else { - block() - } -} - fun Route.adminCreateUser(users: UserRepository) = - post("/api/user/") { + post("/api/user") { runCatching { ifAuthorized { val newUser = call.receiveNullable() ?: throw IllegalArgumentException("No user payload found.") @@ -60,7 +35,7 @@ fun Route.adminCreateUser(users: UserRepository) = } fun Route.adminListUsers(users: UserRepository) = - get("/api/user/") { + get("/api/user") { runCatching { ifAuthorized { call.respond(status = HttpStatusCode.OK, users.getUsers()) diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt new file mode 100644 index 00000000..9e029725 --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt @@ -0,0 +1,43 @@ +package de.berlindroid.zekompanion.server.routers + +import de.berlindroid.zekompanion.server.user.UserRepository +import de.berlindroid.zekompanion.server.zepass.Post +import de.berlindroid.zekompanion.server.zepass.ZePassRepository +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.call +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import io.ktor.server.routing.post + + +fun Route.postPost(zepass: ZePassRepository, users: UserRepository) = + post("/api/zepass") { + runCatching { + val post = call.receive() + + val user = users.getUser(post.posterUUID) + if (user != null) { + val postUUID = zepass.newPost(post) + + call.respond(status = HttpStatusCode.OK, postUUID) + } else { + call.respondText(status = HttpStatusCode.Unauthorized, text = "Nope.") + } + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.getPosts(zepass: ZePassRepository) = + get("/api/zepass") { + runCatching { + call.respond(zepass.getPosts()) + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt new file mode 100644 index 00000000..28c2072e --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt @@ -0,0 +1,50 @@ +package de.berlindroid.zekompanion.server.zepass + +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File +import java.io.FileNotFoundException +import java.util.UUID + +private const val POSTS_FILENAME = "./zepass.db" + +@Serializable +data class Post( + val uuid: String? = null, + val posterUUID: String, + val message: String, +) + +class ZePassRepository private constructor( + private val posts: MutableList = mutableListOf(), +) { + companion object { + fun load(): ZePassRepository = try { + ZePassRepository( + posts = Json.decodeFromString(File(POSTS_FILENAME).readText()), + ) + } catch (notFound: FileNotFoundException) { + ZePassRepository() + } + + fun save(repo: ZePassRepository) = File(POSTS_FILENAME).writer().use { + it.write(Json.encodeToString(repo.posts)) + } + } + + fun newPost(post: Post): String { + val uuid = UUID.randomUUID().toString() + posts.add( + post.copy(uuid = uuid), + ) + + save(this) + + return uuid + } + + fun getPosts(): List { + return posts.toList() + } +} From acacbf73db76aa43c3cd7075f7f858b2b8ccc59e Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Tue, 25 Jun 2024 22:26:21 +0200 Subject: [PATCH 03/26] Add flashing tool This time potentially not only on the bodemanns computer --- zehardware/src/config.py | 9 +- zehardware/src/zeos.py | 3 +- zehardware/zefirmware/zeflash.py | 267 +++++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 zehardware/zefirmware/zeflash.py diff --git a/zehardware/src/config.py b/zehardware/src/config.py index 859bac03..889b3cea 100644 --- a/zehardware/src/config.py +++ b/zehardware/src/config.py @@ -1,3 +1,5 @@ +import util + _SPACE_REPLACEMENT_ = "$SPACE#" @@ -16,13 +18,14 @@ def update_config(config, content: str): print('No content to update.') -def load_config(config, filename: str = '/ze.conf') -> bool: +def load_config(config, filename: str = 'ze.conf') -> bool: try: file = open(filename, 'r') if file: - _execute_assignments_on_obj(config, file.readlines()) + _execute_assignments_on_obj(config, file.read().replace('\n', ' ')) return True - except Exception: + except Exception as e: + print(util.exception_to_readable(e)) return False diff --git a/zehardware/src/zeos.py b/zehardware/src/zeos.py index dd29578c..e2ec9b7b 100644 --- a/zehardware/src/zeos.py +++ b/zehardware/src/zeos.py @@ -63,8 +63,7 @@ def __init__(self): # add defaults self.config = {} - # load_config(self.config) - self.config['uid'] = 'some-thing-amazing' + load_config(self.config) # applications self._app_a = None diff --git a/zehardware/zefirmware/zeflash.py b/zehardware/zefirmware/zeflash.py new file mode 100644 index 00000000..15ce7a04 --- /dev/null +++ b/zehardware/zefirmware/zeflash.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +# +# FLASH ME IF YOU CAN!! +# +import datetime +import os +import requests +import subprocess +import shutil +import time + +OPEN_AI_TOKEN = os.getenv("OPEN_AI_TOKEN") +SERVER_TOKEN = os.getenv("ZESERVER_AUTH_TOKEN") +BASE_URL = "https://zebadge.app/" + +BASE_BADGE_PATH = "/Volumes" + + +def hsv(h, s, v): + """ + For h (0..360), s (0..1), v(0..1) create a r(0..1) g(0..1) b(0..1) tuple. + """ + r = 0 + g = 0 + b = 0 + + c = v * s + x = c * (1.0 - abs((((h / 60.0) % 2.0) - 1.0))) + m = v - c + + if 0 <= h < 60: + r = c + g = x + b = 0 + elif 60 <= h < 120: + r = x + g = c + b = 0 + elif 120 <= h < 180: + r = 0 + g = c + b = x + elif 180 <= h < 240: + r = 0 + g = x + b = c + elif 240 <= h < 300: + r = x + g = 0 + b = c + elif 300 <= h < 360: + r = c + g = 0 + b = x + + return r + m, g + m, b + m + + +def rgb_to_termcolor(rgb): + """ + Convert the given rgb (given from 0 to 1) to a terminal color from 16 to 216 + """ + r, g, b = rgb + return 16 + int(r * 5) * 36 + int(g * 5) * 6 + int(b * 5) + + +def colorize(text): + result = "" + for (index, char) in enumerate(text): + code = rgb_to_termcolor(hsv(index * 360.0 / len(text), 0.3, 1.0)) + result += f"\033[38;5;{code}m{char}" + result += f"\033[m" + return result + + +def request_new_user_id(): + assert SERVER_TOKEN, f"SERVER_AUTH not set: '{SERVER_TOKEN}'." + + users = requests.get( + url=f"{BASE_URL}/api/user", + headers={ + 'Content-Type': 'application/json', + 'ZeAuth': SERVER_TOKEN + }, + ).json() + + print(f"Found {len(users)} users. Adding another one.") + + new_uuid = requests.post( + url=f"{BASE_URL}/api/user", + headers={ + 'Content-Type': 'application/json', + 'ZeAuth': SERVER_TOKEN + }, + json={ + } + ) + + if new_uuid.ok: + new_uuid = new_uuid.json()['uuid'] + print(f".. user '{colorize(new_uuid)}' created.") + else: + print(f".. couldn't create user: '{colorize(new_uuid.text)}'.") + + return new_uuid # server shenanigans + + +def find_mount_point(name): + mounted = subprocess.run(["mount"], capture_output=True, text=True, check=True).stdout + + if name in mounted: + return list(map(lambda y: y.split()[2], filter(lambda x: name in x, mounted.splitlines())))[0] + + else: + return None + + +def find_base_badge_path(): + rpi = find_mount_point('RPI') + if rpi: + return rpi + + cirpy = find_mount_point('CIRCUIT') + if cirpy: + return cirpy + + return None + + +def nuke(): + nuke_ware = list(filter(lambda x: 'nuke' in x, os.listdir("./"))) + + if not nuke_ware: + print(colorize("No nuke firmware found!")) + return False + + nuke_ware = nuke_ware[0] + + path = find_mount_point('RPI') + if not path: + print(f"Please put badge in flash mode.\n{colorize('restart')} ZeBadge and hold {colorize('boot / usr')} button.") + + while not find_mount_point('RPI'): + print('.', end='') + time.sleep(0.1) + print() + path = find_mount_point('RPI') + + print(colorize('nuking')) + + time.sleep(0.2) + shutil.copy(nuke_ware, path) + time.sleep(0.3) + + while not find_mount_point('RPI'): + print('.', end='') + time.sleep(0.1) + print() + + +def flash(): + zepython = list(filter(lambda x: 'zepython' in x, os.listdir("./"))) + + if not zepython: + print(colorize("No zepython firmware found!")) + return False + + zepython = zepython[0] + + path = find_mount_point('RPI') + if not path: + print(f"Please put badge in flash mode.\n{colorize('restart')} and hold {colorize('boot / usr')} button.") + + while not find_mount_point('RPI'): + print('.', end='') + time.sleep(0.1) + print() + + path = find_mount_point('RPI') + + print(colorize('flashing')) + + complete = False + + def wait_on_exception(): + print('x', end='') + time.sleep(1) + + while not complete: + try: + shutil.copy(zepython, path) + complete = True + except PermissionError: + wait_on_exception() + except OSError: + wait_on_exception() + + while not find_mount_point('CIRC'): + print('.', end='') + time.sleep(0.1) + + print() + + +def copy_code(): + circuit = find_mount_point('CIRC') + while not circuit: + print('.', end='') + time.sleep(0.1) + circuit = find_mount_point('CIRC') + + print(colorize("copy libs")) + shutil.copytree('../lib', circuit + "/lib/", dirs_exist_ok=True) + + print(colorize("copy code")) + source_files = os.listdir('../src/') + for src in source_files: + try: + shutil.copy('../src/' + src, circuit) + print('.', end='') + except IsADirectoryError: + print(':', end='') + print() + + print(colorize("copy bitmap resources")) + resource_files = os.listdir('../resources/') + for res in resource_files: + try: + if res.endswith('bmp'): + shutil.copy('../resources/' + res, circuit) + print('.', end='') + else: + print(',', end='') + except IsADirectoryError: + print(':', end='') + + print() + + +def inject_user_id(uuid): + circuit = find_mount_point('CIRC') + while not circuit: + print('.', end='') + time.sleep(0.1) + circuit = find_mount_point('CIRC') + + print(colorize("injecting user id")) + open(circuit + "/ze.conf", "w+").writelines([ + f'uuid={uuid} ', + ]) + + +if __name__ == '__main__': + badge_path = find_base_badge_path() + if badge_path: + print(f"Found a badge on '{colorize(badge_path)}', happy days.") + else: + print(colorize("No Badge found!")) + exit(-1) + + nuke() + flash() + copy_code() + + inject_user_id(request_new_user_id()) + + print(colorize("!! DONE !!")) From 0bbcd0f1cee791efd19f2b23368f70832aa21d25 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Thu, 27 Jun 2024 09:32:45 +0200 Subject: [PATCH 04/26] fix vim issue with vim --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5693d901..e6b63eac 100644 --- a/.gitignore +++ b/.gitignore @@ -559,6 +559,6 @@ obj/ # End of https://www.toptal.com/developers/gitignore/api/python,android,androidstudio,intellij+all,windows,linux,macos,sublimetext,java,kotlin,circuitpython /zeapp/app/release/output-metadata.json /zeapp/app/debug/output-metadata.json -i/zeapp/versions.properties +/zeapp/versions.properties *.db From 4eee802ed4dd6881d2015faac93448e461537c00 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Fri, 28 Jun 2024 17:51:01 +0200 Subject: [PATCH 05/26] Another unused const removed --- .../src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt index 26814a73..84ace8b0 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt @@ -29,9 +29,6 @@ private const val LOCAL_TLS_PORT = 8443 private const val SSL_PASSWORD_ENV = "SSL_CERTIFICATE_PASSWORD" private const val KEYSTORE_RESOURCE_FILE = "/tmp/keystore.jks" -private const val USER_DB_FILE = "/tmp/user.db" - - fun main(args: Array) { val keyPassword = try { System.getenv(SSL_PASSWORD_ENV) From 722d3e3ff9cda220926de2dac860416303bf4339 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Fri, 28 Jun 2024 21:15:12 +0200 Subject: [PATCH 06/26] Downgrade to agp this if for a potential task, and to make my linux machine happy - Mario --- zeapp/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeapp/gradle/libs.versions.toml b/zeapp/gradle/libs.versions.toml index f4f4e7ab..b7e685a3 100644 --- a/zeapp/gradle/libs.versions.toml +++ b/zeapp/gradle/libs.versions.toml @@ -14,7 +14,7 @@ license-report-gradle = { id = "com.jaredsburrows.license", version.ref = "licen [versions] -android-gradle-plugin = "8.3.1" +android-gradle-plugin = "8.2.0" androidx-activity = "1.8.2" androidx-compose-bom = "2024.03.00" androidx-compose-compiler = "1.5.3" From 167de50e00ff58e7df81a73979929e2173bbad4e Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Fri, 28 Jun 2024 21:18:06 +0200 Subject: [PATCH 07/26] add ai framework and initial name generation --- .../de/berlindroid/zekompanion/server/Main.kt | 4 +- .../berlindroid/zekompanion/server/ai/AI.kt | 54 + .../zekompanion/server/routers/Helper.kt | 14 +- .../zekompanion/server/routers/UserRouter.kt | 21 +- .../zekompanion/server/user/UserRepository.kt | 23 +- zeapp/server/src/main/resources/names.txt | 1013 +++++++++++++++++ 6 files changed, 1115 insertions(+), 14 deletions(-) create mode 100644 zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt create mode 100644 zeapp/server/src/main/resources/names.txt diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt index 84ace8b0..c9c07c21 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt @@ -2,6 +2,7 @@ package de.berlindroid.zekompanion.server +import de.berlindroid.zekompanion.server.ai.AI import de.berlindroid.zekompanion.server.routers.imageBin import de.berlindroid.zekompanion.server.routers.imagePng import de.berlindroid.zekompanion.server.routers.index @@ -41,6 +42,7 @@ fun main(args: Array) { println("Serving on port $serverPort.") val users = UserRepository.load() + val ai = AI() val zepass = ZePassRepository.load() embeddedServer( @@ -65,7 +67,7 @@ fun main(args: Array) { imagePng() // Callable from ZeFlasher only? - adminCreateUser(users) + adminCreateUser(users, ai) adminListUsers(users) adminDeleteUser(users) diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt new file mode 100644 index 00000000..a4d711ea --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt @@ -0,0 +1,54 @@ +package de.berlindroid.zekompanion.server.ai + +import java.lang.IndexOutOfBoundsException + +private const val AI_TOKEN_ENV = "AI_AUTH_TOKEN" + +class AI( + private val firstNames: MutableList = mutableListOf(), + private val lastNames: MutableList = mutableListOf(), + private val prefixes: List = listOf( + "Al", + "Bint", + "Ibn", + "Mac", + "Mc", + "Nic", + "Ní", + "da ", + "de ", + "di ", + "le ", + "van ", + "von ", + ), +) { + init { + val names = this.javaClass.classLoader.getResource("names.txt")?.readText() + names?.lines()?.forEachIndexed { index, name -> + try { + val (first, last) = name.split(" ") + if (first !in firstNames) { + firstNames += first + } + + if (last !in lastNames) { + lastNames += last + } + } catch (e: IndexOutOfBoundsException) { + println("Couldn't split name '${name}' at index ${index}. Ignoring it.") + } + } + } + + fun createUserName(): String = "${firstNames.random()} ${createMaybeRandomPrefix()}${lastNames.random()}" + + private fun createMaybeRandomPrefix(): String = when (Math.random()) { + in 0.95..1.0 -> prefixes.random() + else -> "" + } + + fun createUserDescription(name: String): String = "" + + fun createUserImage(name: String, description: String): String = "" +} diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt index c008c39b..ba0ac60a 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt @@ -7,6 +7,8 @@ import io.ktor.server.request.header import io.ktor.server.response.respondText import io.ktor.util.pipeline.PipelineContext +private const val AUTH_TOKEN_ENV = "ZESERVER_AUTH_TOKEN" +private const val AUTH_TOKEN_HEADER = "ZeAuth" suspend fun PipelineContext.withParameter( key: String, @@ -21,9 +23,15 @@ suspend fun PipelineContext.withParameter( } suspend fun PipelineContext.ifAuthorized(block: suspend PipelineContext.() -> Unit) { - val authHeader = call.request.header("ZeAuth") - val authEnv = System.getenv("ZESERVER_AUTH_TOKEN") - if (authEnv.isEmpty() || authHeader == null || authEnv != authHeader) { + val authHeader = call.request.header(AUTH_TOKEN_HEADER) + val authEnv = System.getenv(AUTH_TOKEN_ENV) + + if (authEnv.isNullOrBlank()) { + println("Auth env ('${AUTH_TOKEN_ENV}') environment var is 'null' or empty, you will not be able to do admin level tasks. " + + "Set the env var and restart the server.") + } + + if (authEnv.isNullOrBlank() || authHeader == null || authEnv != authHeader) { call.respondText(status = HttpStatusCode.Forbidden, text = "Forbidden") } else { block() diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt index 9bceead3..535a73a3 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt @@ -1,5 +1,6 @@ package de.berlindroid.zekompanion.server.routers +import de.berlindroid.zekompanion.server.ai.AI import de.berlindroid.zekompanion.server.user.User import de.berlindroid.zekompanion.server.user.UserRepository import io.ktor.http.HttpStatusCode @@ -12,14 +13,26 @@ import io.ktor.server.routing.delete import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.put +import java.util.* -fun Route.adminCreateUser(users: UserRepository) = +fun Route.adminCreateUser(users: UserRepository, ai: AI) = post("/api/user") { runCatching { ifAuthorized { - val newUser = call.receiveNullable() ?: throw IllegalArgumentException("No user payload found.") - val uuidAdded = users.createUser(newUser) + val uuid = UUID.randomUUID().toString() + val name = ai.createUserName() + val description = ai.createUserDescription(name) + val iconb64 = ai.createUserImage(name, description) + + val user = User( + uuid = uuid, + name = name, + description = description, + iconb64 = iconb64, + ) + + val uuidAdded = users.createUser(user) if (uuidAdded != null) { call.respond(status = HttpStatusCode.Created, users.getUser(uuidAdded)!!) @@ -30,7 +43,7 @@ fun Route.adminCreateUser(users: UserRepository) = } }.onFailure { it.printStackTrace() - call.respondText("Error: ${it.message}") + call.respondText("Error: ${it.message}.") } } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt index 216c3833..83fd01ab 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt @@ -1,6 +1,8 @@ package de.berlindroid.zekompanion.server.user +import de.berlindroid.zekompanion.server.ai.AI import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.File @@ -12,18 +14,26 @@ private const val DB_FILENAME = "./user.db" @Serializable data class User( val name: String? = null, - val iconUrl: String? = null, + val iconb64: String? = null, + val description: String? = null, val uuid: String? = null, ) -class UserRepository private constructor( +class UserRepository( private val users: MutableList = mutableListOf(), ) { companion object { fun load(): UserRepository = try { - UserRepository( - users = Json.decodeFromString(File(DB_FILENAME).readText()), - ) + val users = File(DB_FILENAME).readText() + + try { + UserRepository( + users = Json.decodeFromString(users), + ) + } catch (e: SerializationException) { + println("Couldn't read users file. Creating a new one.") + UserRepository() + } } catch (notFound: FileNotFoundException) { UserRepository() } @@ -35,7 +45,8 @@ class UserRepository private constructor( fun createUser(user: User): String? { val existingUser = users.find { it.uuid == user.uuid } - if (existingUser != null || user.uuid != null) { + if (existingUser != null) { + println("User '${user.uuid}' already exists.") return null } diff --git a/zeapp/server/src/main/resources/names.txt b/zeapp/server/src/main/resources/names.txt new file mode 100644 index 00000000..2cc2d58b --- /dev/null +++ b/zeapp/server/src/main/resources/names.txt @@ -0,0 +1,1013 @@ + +Abby Howell +Abdullah Hendricks +Abdullah O’Donnell +Abram Hobbs +Abram Russell +Ada Kirk +Adalee Hahn +Adalee Moyer +Adaline Bond +Adaline Wood +Adalyn Bruce +Adalyn Carey +Adalynn Baker +Adalynn York +Adam Jacobson +Adan Berry +Adan Golden +Adan Porter +Addilyn Villanueva +Addisyn Conley +Adelyn Herrera +Adelynn Banks +Aden Green +Adley Case +Adley Jarvis +Adrianna Esparza +Ahmad Boone +Ahmed Green +Ahmir Lam +Ailani Fry +Ailani Rangel +Ainhoa Scott +Ainsley Reynolds +Aislinn Velasquez +Aitana Weiss +Alaia Peterson +Alan Santana +Alani Wolfe +Alaya Massey +Albert Maynard +Albert McFarland +Aleena Snow +Alena Melton +Alessandro Figueroa +Alessia Galindo +Alexandria Davenport +Alexia Mathews +Alfred Hurst +Ali Reese +Ali Sandoval +Alia Walls +Alianna McMahon +Alice Arellano +Alicia Solis +Alina Spence +Alisen Erdogan +Alison Hayes +Alisson Atkinson +Aliyah Gibson +Allison Francis +Alora Beard +Alora Melendez +Alvin Patterson +Amara Boyle +Amari Bernal +Amari Ibarra +Amelia Cruz +Amelie Jackson +Amina Delacruz +Amirah Castaneda +Amoura Ward +Amy Alfaro +Ander Foley +Ander Franco +Andi Garrison +Andre Richardson +Andrea Camacho +Andrea Huerta +Andrew Medrano +Angel Parsons +Angelo Underwood +Anika Mata +Anika Wise +Aniya Shepherd +Anna Bentley +Anna Sherman +Annabelle Burnett +Annika Cobb +Apollo Greene +Arabella Dixon +Ari Barker +Ari Frye +Ariah Montes +Ariana Hurley +Arianna Valenzuela +Ariel Garza +Ariella Duran +Aries Carey +Ariya Luna +Ariyah McCoy +Ariyah Salazar +Arjun Hancock +Arjun Ortiz +Artemis Pitts +Asa Ayers +Aspyn Norman +Atlas Booth +Atreus Briggs +Atreus Keith +Atreus Rice +Atticus Dickson +Aubree Becker +Aubree Lee +Aubrie Atkins +Aubrie Roberts +Aubriella Robertson +Aubriella Sparks +Audrey Roberson +August Patton +Augustus Lowe +Aurora Rice +Aurora Romero +Austin Bernal +Avi Stark +Axton Novak +Ayaan Sharp +Aydin White +Ayla Morgan +Aylin Sellers +Azariah Hubbard +Azariah Melton +Aziel Mahoney +Barbara Taylor +Baylor McCoy +Beatrice Miles +Beatrice Wood +Beckett Mathews +Bella Bodemann +Bellamy McCormick +Bellamy Robinson +Benjamin Porter +Bentlee Moses +Berkley Osborne +Bishop McDowell +Bishop Vaughan +Bobby Lee +Bode Jordan +Bonnie Browning +Bradley Stark +Braelyn Montes +Braxton Allen +Braxton Mayer +Braylee Goodwin +Braylee Simpson +Brendan Vang +Bria Watkins +Brianna Lester +Briar Kemp +Bridger Hull +Bridget Glover +Briggs Potts +Brinley Cole +Bristol Knight +Bristol Nichols +Brixton Underwood +Brody Reyna +Bruce Escobar +Bryce Powell +Brylee Hull +Brynn Sharp +Bryson Stevens +Caleb Whitney +Callahan Sherman +Calum Long +Camden Blair +Cameron Stout +Camryn Ahmed +Cannon Daniel +Carl Pierce +Carly Brandt +Carly Figueroa +Carly Marquez +Carmelo Poole +Carolina Doyle +Carolina Miranda +Caroline Valdez +Carolyn Colon +Carolyn Sosa +Carson Hayde +Carson Rubio +Carson Vargas +Carter Bennett +Case Barrera +Case McPherson +Casen Garcia +Casen Hicks +Casen Jefferson +Cason Archer +Cason Avery +Cassandra Gross +Cassandra Liu +Castiel Avalos +Cataleya Williamson +Catherine Graham +Catherine Moody +Cayden Schmitt +Cedric Blanchard +Celeste Gilmore +Celine Campbell +Cesar McPherson +Charleigh Myers +Charles Callahan +Charlotte Bates +Chaya Long +Christian Roberts +Christina Maddox +Christopher Richmond +Christopher Singh +Cillian Hartman +Cketti Cketti +Claire Ruiz +Clara Tran +Clare Garza +Clay House +Clay Vo +Cleo Combs +Cleo Mejia +Coen Rosario +Cohen Hodges +Cohen Meza +Cole Hurst +Colin Moran +Colin Snow +Collin McCarthy +Collin Waters +Collins Randall +Collins Randolph +Colt Hodges +Conner Gibbs +Connor Murillo +Conrad Bernal +Conrad Rhodes +Cooper Hawkins +Cooper Pittman +Cora Paul +Crew Ibarra +Cruz Lawrence +Cullen Allen +Cullen Boone +Daisy Curry +Dalary Santana +Dalary Santana +Daleyza Bruce +Damian Booth +Damian Leal +Damian Sloan +Damien Pratt +Damir Scott +Damir Walters +Dane Woodard +Dani Greer +Dariel Ross +Dariel Vang +Dario Hahn +Darius Mills +Darren Flowers +Darren Glass +Darren Wallace +Dash Palmer +David Macias +Davina Hebert +Davis Moyer +Deandre Hardin +Deborah Garner +Delilah Mueller +Denise Gregory +Dennis Richard +Denver Waters +Destiny Stevens +Dillon Reyes +Dima Dima +Dominic Woodward +Dominik Woodard +Donald Wu +Donovan Glover +Donovan Johns +Dorothy Floyd +Douglas Mora +Drake Park +Drew Carey +Drew Hutchinson +Duke Garza +Duncan Ward +Duncan Watson +Dustin Henderson +Easton McKee +Easton Pitts +Edith Arellano +Edith Zavala +Eduardo Henson +Edward Pratt +Eithan Espinoza +Eithan Keith +Eithan Nichols +Eli Humphrey +Elisa Garcia +Eliseo Alvarez +Eliseo Delacruz +Eliza Mosley +Elle Olson +Ellen Waters +Elliot Lopez +Elliott Blackwell +Elliott Merritt +Ellis McBride +Ellis Roth +Ellison Hensley +Elsa Carroll +Elsie Bradford +Elyse Gibson +Elyse Pugh +Elyse Shepherd +Ember Macias +Ember Vazquez +Emberly Jarvis +Emely Dillon +Emerson Martinez +Emersyn Novak +Emiliano Dominguez +Emir Russell +Emmaline Gilmore +Emmaline Hayden +Emmalynn Weiss +Emmeline Rojas +Emmeline Stark +Emmett Coleman +Emmett Davenport +Emmy Stokes +Ensley Barnes +Ensley Mahoney +Erick Parsons +Erick Walter +Erik Byrd +Erik Kent +Erin Kelly +Esperanza Gill +Eugene Alvarez +Eugene Barry +Eve Montoya +Eve Underwood +Evelynn Nicholson +Evie Boyer +Ezra Villegas +Fabian Bradley +Faith Leonard +Fallon Cervantes +Faye Dyer +Felipe Aguirre +Fernando Hobbs +Fernando Wade +Finley Chang +Finley Cordova +Florence Reed +Ford Quinn +Forrest Daniels +Frances Barnes +Frances Finley +Francesca Morrison +Francisco Barry +Franco Long +Frankie Marquez +Franklin Greer +Frederick Jones +Freya Santana +Freyja Montes +Frida Nguyen +Gabriel Salinas +Gabriel Tran +Galilea Carter +Georgia Vazquez +Gia Calderon +Gianna Kennedy +Giavanna Olson +Giovanna Russo +Giovanni Singh +Giselle Garza +Giselle Goodwin +Giuliana Goodman +Giuliana Underwood +Gloria Matthews +Gracelyn Kemp +Gracelyn Terrell +Gracelynn Roth +Gracie Maldonado +Graham Daniels +Graysen Burch +Graysen Reyna +Gregory Randall +Grey Bernard +Grey Murray +Greyson Caldwell +Greyson Carpenter +Guillermo Ramos +Gunnar Maynard +Gunnar Wood +Hadassah Davenport +Hadassah Stokes +Hailey McIntyre +Haley Figueroa +Halle Carrillo +Halo Hendricks +Hamza Kirk +Harlan Ortiz +Harlow O’brien +Harmony Butler +Harper Walker +Harry Robertson +Harry Shaw +Harvey Juarez +Hasan Hosgel +Hattie Mercado +Hattie Zuniga +Hayden Conway +Heath Moon +Heath Pearson +Heaven Terrell +Holland Morse +Hope Huang +Houston Arroyo +Hunter Fuentes +Huxley Larsen +Huxley Quintero +Isaac Horton +Isaias Benson +Ismael Barr +Ismael Cunningham +Ivy Burns +Ivy Kirby +Jace Calhoun +Jace Charles +Jack Donovan +Jack Esquivel +Jackson Parra +Jacob Miranda +Jacoby Everett +Jacoby Monroe +Jacqueline Ford +Jad Barker +Jade Gardner +Jade McClure +Jaden Byrd +Jaelynn Drake +Jake House +Jakob Anthony +Jakob Golden +Jalen Cervantes +Jalen Pitts +Jamal Michael +Jamari Quintero +James Bond +James Webster +Jameson Barrett +Jamie Dawson +Jamir Ortega +Jane Greer +Janelle Hoover +Jared Moses +Jasiah Garner +Javad Javad +Javier Fuentes +Jaxen Owen +Jaxen Page +Jaylee Duarte +Jayson Edwards +Jaziel Chan +Jazlyn Ali +Jazmine Garrison +Jeffrey Rojas +Jemma Vaughan +Jenna Koch +Jenna Vance +Jennifer Smith +Jeremy Barry +Jesse Ashley +Jesse Roy +Jesse Simon +Jessie McCann +Jett Randolph +Jimena Sherman +Jimmy Lam +Joel Cohen +Joel Palacios +Johan Mahoney +Jon Baxter +Jordan May +Jordyn Grant +Jordyn Novak +Josiah Brewer +Journee Lindsey +Journi Simpson +Jovanni Pruitt +Joy Sherman +Judah Cabrera +Judah Goodman +Judah Lane +Judith Hood +Judson James +Judson Monroe +Julia Copeland +Julia Pacheco +Julian Leblanc +Julian Rhodes +Julien Hancock +Juliet Hudson +Julieta Jefferson +June Bell +June Vang +Junior Pacheco +Juniper Branch +Justice Bowman +Justice Sellers +Justin Crosby +Kabir Pacheco +Kade Daugherty +Kadence Booker +Kai Dominguez +Kailey Coffey +Kaisley Austin +Kaison Cisneros +Kaison Suarez +Kaiya Le +Kaiya Wolfe +Kalani Weber +Kali Cox +Kamari Bullock +Kamari Goodman +Kamilah Bass +Kamilah Davila +Kamiyah Watson +Kamryn Allen +Kane Guerra +Karina Ibarra +Karina Owen +Karsyn Page +Karter Charles +Karter Morrow +Kase Cline +Kashton Padilla +Katelyn Benton +Katelyn Cruz +Katherine Watkins +Kaylee Lucero +Kayleigh Callahan +Kaylie Munoz +Kayson Phelps +Keenan Guerra +Kehlani Castaneda +Keily McGee +Keily Mendoza +Kellan Boyd +Kellan Osborne +Kelly Hernandez +Kelly Watkins +Kelsey Wang +Kendall Tran +Kennedi Morrison +Kensley McDaniel +Kenzie Pena +Kenzo Zimmerman +Keyla Hurley +Keyla McKenzie +Khalani Rosas +Khari Gentry +Kian Chen +Kiara Richmond +Kiara Villanueva +Kimber Underwood +Kimberly Summers +Kinslee Chapman +Kira Graves +Knox Rogers +Koa Jennings +Koa Kennedy +Kobe Koch +Koda Hanson +Koda Navarro +Kody Stafford +Kora Duran +Korbin Vaughan +Korbyn Pennington +Kori Winters +Kristopher George +Kyle Ellison +Kyler Grimes +Kylie Morrow +Kynlee Leach +Kyra Choi +Kyree Keith +Kyree McGee +Kyree Pennington +Kyree Spence +Lacey Matthews +Lacey Shaw +Lainey Randolph +Lainey Santos +Lana Pollard +Landen Delgado +Landry Pugh +Laney Carter +Lara Cross +Lara Joseph +Larry Marshall +Lauren Harvey +Lauren Melendez +Lawrence Boyer +Lawrence Mendoza +Lawson Casey +Lawson Newman +Layla Love +Layla Stanley +Layne Gaines +Layne Tate +Leah Guerrero +Leandro Benson +Lee Chung +Legend Mann +Leilani Avalos +Leilani Vance +Lennon Ball +Lennon Hensley +Leo Lynn +Leon Espinosa +Leonardo Lim +Leroy Espinosa +Lewis Gallegos +Lewis Gregory +Lia Farley +Liam Santiago +Liana Gregory +Lilah Patton +Lilian Lu +Lilith Castillo +Lillian Ballard +Lilly Phan +Lina Hopkins +Lina Parker +Lina Wade +Lorelai Villa +Lorelai Zhang +Lorelei Long +Louis Lyons +Louis Novak +Louis Tsai +Louisa Briggs +Lucia Beck +Lucille Dyer +Lucille Lee +Lucy McCann +Luella Charles +Luella Hudson +Luis Beck +Luke Bradford +Luke Richards +Lyanna Rose +Lyric Baxter +Mabel Barnes +Mac Roach +Macie Blevins +Mack Orozco +Macy Thompson +Madalynn Singh +Madden Graves +Madden Stephens +Maddison Luna +Madeleine Bernal +Madeleine Hart +Madelyn Daugherty +Madilynn Hess +Madilynn McMahon +Madisyn Kelly +Madisyn Serrano +Magdalena Dean +Maggie Phan +Maia Rojas +Maisie Villa +Maison Carter +Maison Knight +Major Blackburn +Makai White +Malachi Espinoza +Malachi Harmon +Malakai Ellis +Malani Parsons +Malaya Carroll +Maleah Vance +Manuel Chan +Marcus Arellano +Maren Miller +Maria McClure +Mariam English +Mariam Griffith +Mariam Wood +Mariana Garcia +Marie Drake +Marie Richard +Mario Bodemann +Marley Fuller +Marlon Gillespie +Marlon Guevara +Martin Young +Marvin Norman +Mason Bravo +Mathew Monroe +Matthew Holt +Matthias Geisler +Matthias Logan +Matthias Marshall +Maverick Duffy +Maverick O’Donnell +Mavis Sutton +Maximilian Kelley +Maximus Graham +Maximus Phan +Maxwell Wood +Mckenzie Ortega +Meghan Fry +Meghan Patel +Melany Todd +Melvin Gardner +Memphis Rangel +Mercy Bernal +Mercy Rivera +Mikaela Cole +Mikayla Bonilla +Mikayla Neal +Milan Lloyd +Milana Barrera +Milana Fleming +Milani Robles +Millie Esquivel +Milos Marinkovic +Mina Anderson +Miro Miro +Mitchell Santana +Mohamed Donovan +Mohamed Madden +Mohamed Silva +Mohammad Dennis +Moises Cruz +Mordechai Kaur +Moshe Atkins +Muhammad Boone +Murphy McBride +Murphy Payne +Myra King +Myra Wilkerson +Nala Bishop +Nala Dyer +Nala Newton +Nancy Allison +Nancy Campbell +Nancy Pope +Nash Case +Nash Crawford +Nash Lang +Natalia Buck +Natalia Frederick +Nathanael English +Nathaniel Michael +Nathaniel Reese +Naya Blackburn +Nicolas Pittman +Nikolas Burnett +Nikolas Colon +Noah Brandt +Noe Gallegos +Noe Vazquez +Noel McConnell +Noelle Leal +Noemi Gill +Novalee Preston +Nyla Frost +Oakleigh O’Connell +Oakley Frye +Oaklynn Benitez +Oaklynn Wang +Omar Zimmerman +Ophelia Morrow +Oscar Cantu +Oscar Craig +Otto Villarreal +Pablo Rush +Paislee Hess +Paisley Trujillo +Palmer McClain +Paloma Hoffman +Paris Potts +Paris Roberson +Parker Nixon +Paxton Dougherty +Pedro McCarty +Penny Yoder +Peter Huff +Peter Rubio +Peyton Morrow +Philip Jordan +Phoebe Nava +Phoenix Leal +Pierce French +Preston Hardin +Preston Mills +Promise Delgado +Promise Woodard +Queen Cortez +Quincy Fernandez +Quinn Koch +Quinn McIntyre +Quinton Hale +Raegan Franklin +Raegan Hart +Raegan Logan +Raegan Pacheco +Raelynn Ellis +Raelynn Singh +Rafael Stanley +Raina McCullough +Randy Sosa +Raphael Gardner +Ray Hail +Raya Stevens +Rayden Sellers +Raylan Dalton +Rayna Farley +Reagan Travis +Reece Khan +Reece Peterson +Reece Richards +Reese Carlson +Reese Scott +Regina Mercado +Reign Sosa +Reina Becker +Remi Schaefer +Remington Lu +Remy Foley +Renata Grimes +Rhea Hobbs +Ricardo Ali +Richard Charles +Ricky French +Riley Barrett +Riley Lynn +Riley Roman +Riley Vargas +River Coleman +River Gould +River Hanna +Rivka Meyers +Robin Lawrence +Rocco Simmons +Rodrigo Mitchell +Roger Stanley +Rohan Elliott +Ronald Manning +Ronald Williamson +Ronan Perry +Ronin Harding +Rory Carey +Rosa Suarez +Rosalie Morris +Rosemary Hendrix +Rosemary Olsen +Rowen Rasmussen +Roy Melton +Roy Nicholson +Royal Atkins +Royalty King +Royce Dominguez +Ryan Duarte +Ryan Williamson +Ryann Barron +Ryder Barrera +Ryland Molina +Ryleigh Nguyen +Ryleigh Vazquez +Sabrina Hodge +Sage Macias +Sage Valencia +Saint Flynn +Salem Lugo +Salvador Rivera +Salvatore McDonald +Samara Petersen +Samir Rojas +Samira Ramirez +Samson Andersen +Santana Crawford +Santana Owen +Santiago Morgan +Santino Joseph +Saoirse Phillips +Sarah Duarte +Sariah Bell +Sariah Bradley +Savanna Cervantes +Savanna Correa +Sawyer Snyder +Scarlett Briggs +Scott Reeves +Scout Barber +Sebastian Lawson +Selena Fleming +Selene Garner +Shelby Pennington +Shepherd Hail +Shepherd Pugh +Simon Jensen +Simone Ahmed +Sincere Hall +Skye Watson +Skyler Bradshaw +Skyler Hester +Skyler Kemp +Sloan Larson +Solomon Woodward +Sophia Davila +Soren Gross +Spencer Abbott +Spencer Salazar +Stefan Vaughan +Steven Crosby +Sullivan Cline +Sylvia Olsen +Talon Huang +Tatum Blair +Tatum Christensen +Tatum Hinton +Teresa Estrada +Thalia Blake +Thea Herrera +Theodore Brown +Thiago Cline +Thiago McMillan +Tony Hinton +Travis Bowers +Travis Gray +Trenton Estrada +Trey Munoz +Trinity Beltran +Trinity Dunlap +Turner Barnett +Tyler Gill +Tyler Webb +Uriah Bauer +Uriah Hansen +Vada Pope +Vada Reyes +Valentin Pearson +Valeria Curtis +Van Dominguez +Van Newman +Vanessa Padilla +Veronica Knox +Vihaan English +Vihaan Schmidt +Vincent Erickson +Vincenzo Roy +Violeta Wiley +Violette Pratt +Vivian Hopkins +Vivienne Cline +Vivienne Poole +Wade Pitts +Walker Truong +Warren Nava +Watson McCormick +Watson Parks +Waverly Blackburn +Waverly Hill +Waverly Snyder +Westin Andrade +Westley Sexton +Whitney Carpenter +Willie Case +Willow Mueller +Winnie Day +Winter Huber +Xiomara Brewer +Xzavier Romero +Yareli McFarland +Yareli Tapia +Zachary Decker +Zachary Villalobos +Zahir Griffith +Zahir Parra +Zakai Curry +Zariyah Cline +Zariyah Pierce +Zaylee Walker +Zayn Edwards +Zechariah Schroeder +Zeke Stevenson +Zelda Reed +Zendaya May +Ziggy Fartface +Zoe Ali +Zoe Bradford +Zoey Riley +Zoie Olsen +Zola Phelps +Zoya Sellers +Zyaire Friedman From df48f64da8aacceabe7b0ee60869f82a35d42ac2 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Fri, 28 Jun 2024 22:33:52 +0200 Subject: [PATCH 08/26] add gemini for descriptions --- zeapp/gradle/libs.versions.toml | 3 +- zeapp/server/build.gradle.kts | 2 + .../berlindroid/zekompanion/server/ai/AI.kt | 8 +- .../zekompanion/server/ai/Gemini.kt | 109 ++++++++++++++++++ 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt diff --git a/zeapp/gradle/libs.versions.toml b/zeapp/gradle/libs.versions.toml index b7e685a3..270d2776 100644 --- a/zeapp/gradle/libs.versions.toml +++ b/zeapp/gradle/libs.versions.toml @@ -38,7 +38,7 @@ license-report-gradle = "0.9.3" material = "1.11.0" material3-wsc = "1.2.1" mik3y-usb-serial = "3.7.0" -retrofit = "2.9.0" +retrofit = "2.11.0" sl4j = "2.0.12" timber = "5.0.1" uiautomator = "2.3.0" @@ -91,6 +91,7 @@ material = { group = "com.google.android.material", name = "material", version.r material3-wsc = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "material3-wsc" } mik3y-usb-serial-android = { module = "com.github.mik3y:usb-serial-for-android", version.ref = "mik3y-usb-serial" } retrofit2-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } +retrofit2-converter-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } retrofit2-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } zxing = { module = "com.google.zxing:core", version.ref = "zxing" } diff --git a/zeapp/server/build.gradle.kts b/zeapp/server/build.gradle.kts index 82feccd8..b497c249 100644 --- a/zeapp/server/build.gradle.kts +++ b/zeapp/server/build.gradle.kts @@ -23,6 +23,8 @@ dependencies { implementation(libs.ktor.content.serialization) implementation(libs.kotlinx.serialization.json) implementation(libs.sl4j.simple) + implementation(libs.retrofit2.retrofit) + implementation(libs.retrofit2.converter.serialization) } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt index a4d711ea..a920a872 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt @@ -2,9 +2,9 @@ package de.berlindroid.zekompanion.server.ai import java.lang.IndexOutOfBoundsException -private const val AI_TOKEN_ENV = "AI_AUTH_TOKEN" class AI( + private val gemini: Gemini = Gemini(), private val firstNames: MutableList = mutableListOf(), private val lastNames: MutableList = mutableListOf(), private val prefixes: List = listOf( @@ -41,14 +41,14 @@ class AI( } } - fun createUserName(): String = "${firstNames.random()} ${createMaybeRandomPrefix()}${lastNames.random()}" + suspend fun createUserName(): String = "${firstNames.random()} ${createMaybeRandomPrefix()}${lastNames.random()}" private fun createMaybeRandomPrefix(): String = when (Math.random()) { in 0.95..1.0 -> prefixes.random() else -> "" } - fun createUserDescription(name: String): String = "" + suspend fun createUserDescription(name: String): String = gemini.getDescription(name) - fun createUserImage(name: String, description: String): String = "" + suspend fun createUserImage(name: String, description: String): String = "" } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt new file mode 100644 index 00000000..e6dffe1b --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt @@ -0,0 +1,109 @@ +package de.berlindroid.zekompanion.server.ai + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.MediaType +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Query + +private const val AI_TOKEN_ENV = "AI_AUTH_TOKEN" + +@Serializable +data class PromptBody( + val contents: List, +) { + @Serializable + data class Content( + val parts: List, + ) { + @Serializable + data class Part( + val text: String, + ) + } +} + +@Serializable +data class PromptResponse( + val candidates: List, +) { + @Serializable + data class Candidate( + val content: Content, + val finishReason: String, + val index: Int, + ) { + @Serializable + data class Content( + val parts: List, + val role: String, + ) { + @Serializable + data class Part( + val text: String, + ) + } + } +} + +interface SimpleGeminiService { + @POST("v1beta/models/gemini-1.5-flash:generateContent") + suspend fun prompt( + @Query("key") key: String, + @Body prompt: PromptBody, + ): PromptResponse +} + + +class Gemini( + private val json: Json = Json { ignoreUnknownKeys = true }, + private val service: SimpleGeminiService = Retrofit.Builder() + .baseUrl("https://generativelanguage.googleapis.com/") + .addConverterFactory( + json.asConverterFactory( + MediaType.parse("application/json; charset=UTF8")!!, + ), + ) + .build() + .create(SimpleGeminiService::class.java), + private val token: String = System.getenv(AI_TOKEN_ENV) ?: "", +) { + + suspend fun getDescription(name: String): String { + val prompt = PromptBody( + contents = listOf( + PromptBody.Content( + parts = listOf( + PromptBody.Content.Part( + text = "You are a dungeons and dragons dungeon master, building a dnd session at the Droidcon in Berlin in 2024. You'll answer with a 100 words description of characters backgrounds.", + ), + PromptBody.Content.Part( + text = "A new player joins: \"${name}\". Please give me a background description of this character playing at the Droidcon in Berlin 2024.", + ), + ), + ), + ), + ) + + try { + val response = service.prompt( + token, + prompt, + ) + + return response.candidates.joinToString(separator = ",") { candidate -> + candidate.content.parts.joinToString { part -> + part.text + } + } + } catch (e: Exception) { + e.printStackTrace() + print("Couldn't gemini!") + return "" + } + + } +} From 3d686ac927e03f8e77f57e065b973bb049c04b53 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sat, 29 Jun 2024 00:06:32 +0200 Subject: [PATCH 09/26] Add user image generation. --- .../berlindroid/zekompanion/server/ai/AI.kt | 6 +- .../zekompanion/server/ai/Dalle.kt | 107 ++++++++++++++++++ .../zekompanion/server/ext/ImageExt.kt | 2 +- 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt index a920a872..a002fed0 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt @@ -5,6 +5,7 @@ import java.lang.IndexOutOfBoundsException class AI( private val gemini: Gemini = Gemini(), + private val dale: Dalle = Dalle(), private val firstNames: MutableList = mutableListOf(), private val lastNames: MutableList = mutableListOf(), private val prefixes: List = listOf( @@ -50,5 +51,8 @@ class AI( suspend fun createUserDescription(name: String): String = gemini.getDescription(name) - suspend fun createUserImage(name: String, description: String): String = "" + suspend fun createUserImage(name: String, description: String): String = dale.requestImageGeneration( + name = name, + description = description, + ) ?: "" } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt new file mode 100644 index 00000000..10765f9c --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt @@ -0,0 +1,107 @@ +package de.berlindroid.zekompanion.server.ai + +import de.berlindroid.zekompanion.* +import de.berlindroid.zekompanion.server.ext.ImageExt.toPixels +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.MediaType +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST +import java.net.URL +import java.util.concurrent.TimeUnit +import javax.imageio.ImageIO + +private const val OPENAI_TOKEN_ENV = "DALE_AUTH_TOKEN" + + +@Serializable +data class ImagePrompt( + val prompt: String, + @SerialName("n") val imageCount: Int = 1, + val size: String = "256x256", +) + +@Serializable +data class GeneratedImages( + val created: Int, + val data: List, +) { + @Serializable + data class ImageLocation( + val url: String, + ) +} + +interface OpenAIService { + @POST("/v1/images/generations") + suspend fun generateImage( + @Header("Content-Type") contentType: String = "application/json", + @Header("Authorization") authorization: String, + @Body prompt: ImagePrompt, + ): GeneratedImages +} + +private const val TIMEOUT: Long = 90 + +class Dalle( + private val json: Json = Json { ignoreUnknownKeys = true }, + private val service: OpenAIService = Retrofit.Builder() + .baseUrl("https://api.openai.com/") + .addConverterFactory( + json.asConverterFactory( + MediaType.parse("application/json; charset=UTF8")!!, + ), + ) + .client( + OkHttpClient().newBuilder() + .callTimeout(TIMEOUT, TimeUnit.SECONDS) + .readTimeout(TIMEOUT, TimeUnit.SECONDS) + .writeTimeout(TIMEOUT, TimeUnit.SECONDS) + .connectTimeout(TIMEOUT, TimeUnit.SECONDS) + .build(), + ) + .build() + .create(OpenAIService::class.java), + + private val token: String = System.getenv(OPENAI_TOKEN_ENV) ?: ("" + println("OAI token not found!")), +) { + + suspend fun requestImageGeneration( + name: String, + description: String, + ): String? { + try { + val maybeImages = service.generateImage( + prompt = ImagePrompt( + prompt = "Please draw me a black and white picture of \"${name}\". " + + "They can be described as follows: ${description}. Thank you.", + ), + authorization = "Bearer $token", + ) + + val location = maybeImages.data.firstOrNull() ?: return null + + val image = ImageIO.read(URL(location.url)) + val width = image.width + val height = image.height + val pixels = image + .toPixels() + .resize(width, height, 32, 32) + .ditherFloydSteinberg(32, 32) + .toBinary() + .zipit() + .base64() + + return pixels + } catch (e: Exception) { + e.printStackTrace() + println("Could not generate image!") + return null + } + } +} diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ext/ImageExt.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ext/ImageExt.kt index 957a210b..6b4a1105 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ext/ImageExt.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ext/ImageExt.kt @@ -15,7 +15,7 @@ import java.nio.IntBuffer import javax.imageio.ImageIO object ImageExt { - private fun BufferedImage.toPixels(): IntBuffer { + fun BufferedImage.toPixels(): IntBuffer { val output = IntBuffer.allocate(width * height) getRGB(0, 0, width, height, output.array(), 0, width) return output From 8e416909b7cc954cdc7511c3e2e2d85e25a92f6b Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sat, 29 Jun 2024 00:06:57 +0200 Subject: [PATCH 10/26] Fix: Empty line in names.txt. --- zeapp/server/src/main/resources/names.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/zeapp/server/src/main/resources/names.txt b/zeapp/server/src/main/resources/names.txt index 2cc2d58b..21f1c2e9 100644 --- a/zeapp/server/src/main/resources/names.txt +++ b/zeapp/server/src/main/resources/names.txt @@ -1,4 +1,3 @@ - Abby Howell Abdullah Hendricks Abdullah O’Donnell From 92fc89868336067d36c758128c1d339c1ba3983c Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sat, 29 Jun 2024 02:14:24 +0200 Subject: [PATCH 11/26] Minor tweaks to image generation update the prompt and add png export f(x). --- .../zekompanion/BinaryManipulation.kt | 16 ++++++ .../de/berlindroid/zekompanion/BitmapTest.kt | 50 +++++++++++++++++ .../de/berlindroid/zekompanion/server/Main.kt | 13 +---- .../zekompanion/server/ai/Dalle.kt | 27 +++++---- .../zekompanion/server/routers/UserRouter.kt | 56 +++++++++++++++++-- .../zekompanion/server/user/UserRepository.kt | 15 ++--- 6 files changed, 141 insertions(+), 36 deletions(-) diff --git a/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BinaryManipulation.kt b/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BinaryManipulation.kt index 88b8ae2d..5281ead4 100644 --- a/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BinaryManipulation.kt +++ b/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BinaryManipulation.kt @@ -195,7 +195,23 @@ fun IntBuffer.toBinary(): ByteBuffer { return output.toBuffer() } +/** + * Take a binary bytebuffer and convert it into an int buffer. + */ +fun ByteBuffer.fromBinaryToRGB(): IntBuffer { + val output = mutableListOf() + forEach { byte -> + for (index in 0 until 8) { + val bit = (byte.toInt() shr (7 - index)) and 1 + output.add(rgb(bit * 255, bit * 255, bit * 255)) + } + } + + return output.toBuffer() +} + private fun List.toBuffer(): ByteBuffer = ByteBuffer.wrap(toTypedArray().toByteArray()) +private fun List.toBuffer(): IntBuffer = IntBuffer.wrap(toTypedArray().toIntArray()) /** * Map all values of an IntBuffer diff --git a/zeapp/badge/src/commonTest/kotlin/de/berlindroid/zekompanion/BitmapTest.kt b/zeapp/badge/src/commonTest/kotlin/de/berlindroid/zekompanion/BitmapTest.kt index 93c75e7c..624bc8c8 100644 --- a/zeapp/badge/src/commonTest/kotlin/de/berlindroid/zekompanion/BitmapTest.kt +++ b/zeapp/badge/src/commonTest/kotlin/de/berlindroid/zekompanion/BitmapTest.kt @@ -10,6 +10,7 @@ import java.awt.image.BufferedImage import java.awt.image.BufferedImage.TYPE_INT_RGB import java.io.File import java.io.FileNotFoundException +import java.nio.ByteBuffer import java.nio.IntBuffer import javax.imageio.ImageIO @@ -84,6 +85,55 @@ class BitmapTest { assertThat(output).isEqualTo("eNpjYHBYIMHHcHCBV/19ABJOBBA=") } + + @Test + fun binaryBitMaskToRGBImageSmall() { + val w = rgb(255, 255, 255) + val b = rgb(0, 0, 0) + + val width = 4 + val height = 2 + + val input = IntBuffer.wrap( + intArrayOf( + w, b, w, b, + b, w, b, w, + ), + ) + + val bits = input.toBinary() + + val output = bits.fromBinaryToRGB() + + assertThat(input.toBufferedImage(width, height)).allPixelEqualTo(output.toBufferedImage(width, height)) + + } + + @Test + fun binaryBitMaskToRGBImageHuge() { + val w = rgb(255, 255, 255) + val b = rgb(0, 0, 0) + + val width = 8 + val height = 5 + + val input = IntBuffer.wrap( + intArrayOf( + b, b, b, b, b, b, b, b, b, + b, b, w, b, b, b, w, b, b, + b, b, w, w, b, w, w, b, b, + b, b, w, b, w, b, w, b, b, + b, b, w, b, b, b, w, b, b, + ), + ) + + val bits = input.toBinary() + + val output = bits.fromBinaryToRGB() + + assertThat(input.toBufferedImage(width, height)).allPixelEqualTo(output.toBufferedImage(width, height)) + + } } fun Assert.allPixelEqualTo(other: BufferedImage) = given { actual -> diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt index c9c07c21..1cf97678 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt @@ -3,16 +3,7 @@ package de.berlindroid.zekompanion.server import de.berlindroid.zekompanion.server.ai.AI -import de.berlindroid.zekompanion.server.routers.imageBin -import de.berlindroid.zekompanion.server.routers.imagePng -import de.berlindroid.zekompanion.server.routers.index -import de.berlindroid.zekompanion.server.routers.adminCreateUser -import de.berlindroid.zekompanion.server.routers.adminDeleteUser -import de.berlindroid.zekompanion.server.routers.adminListUsers -import de.berlindroid.zekompanion.server.routers.getPosts -import de.berlindroid.zekompanion.server.routers.getUser -import de.berlindroid.zekompanion.server.routers.postPost -import de.berlindroid.zekompanion.server.routers.updateUser +import de.berlindroid.zekompanion.server.routers.* import de.berlindroid.zekompanion.server.user.UserRepository import de.berlindroid.zekompanion.server.zepass.ZePassRepository import io.ktor.serialization.kotlinx.json.* @@ -74,6 +65,8 @@ fun main(args: Array) { // TODO: Check if callable from ZeBadge (no ssl) updateUser(users) getUser(users) + getUserProfileImageBinary(users) + getUserProfileImagePng(users) postPost(zepass, users) getPosts(zepass) diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt index 10765f9c..8f105c3f 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt @@ -20,10 +20,11 @@ private const val OPENAI_TOKEN_ENV = "DALE_AUTH_TOKEN" @Serializable -data class ImagePrompt( - val prompt: String, +data class ImageRequest( @SerialName("n") val imageCount: Int = 1, val size: String = "256x256", + val model: String, + val prompt: String, ) @Serializable @@ -42,12 +43,14 @@ interface OpenAIService { suspend fun generateImage( @Header("Content-Type") contentType: String = "application/json", @Header("Authorization") authorization: String, - @Body prompt: ImagePrompt, + @Body request: ImageRequest, ): GeneratedImages } private const val TIMEOUT: Long = 90 +const val USER_PROFILE_PICTURE_SIZE = 32 + class Dalle( private val json: Json = Json { ignoreUnknownKeys = true }, private val service: OpenAIService = Retrofit.Builder() @@ -70,34 +73,36 @@ class Dalle( private val token: String = System.getenv(OPENAI_TOKEN_ENV) ?: ("" + println("OAI token not found!")), ) { - suspend fun requestImageGeneration( name: String, description: String, ): String? { try { val maybeImages = service.generateImage( - prompt = ImagePrompt( - prompt = "Please draw me a black and white picture of \"${name}\". " + - "They can be described as follows: ${description}. Thank you.", + request = ImageRequest( + model = "dall-e-3", + prompt = "Please create a digital picture of \"${name}\", a player character of a black and white pixelated game. " + + "The picture should show them in action doing their favorite thing, it should be isometric. " + + "$name can be described as follows: '${description}'.", ), authorization = "Bearer $token", ) val location = maybeImages.data.firstOrNull() ?: return null + println("Avatar of '$name' generated at ${location.url}.") val image = ImageIO.read(URL(location.url)) val width = image.width val height = image.height - val pixels = image + val b64 = image .toPixels() - .resize(width, height, 32, 32) - .ditherFloydSteinberg(32, 32) + .resize(width, height, USER_PROFILE_PICTURE_SIZE, USER_PROFILE_PICTURE_SIZE) + .ditherFloydSteinberg(USER_PROFILE_PICTURE_SIZE, USER_PROFILE_PICTURE_SIZE) .toBinary() .zipit() .base64() - return pixels + return b64 } catch (e: Exception) { e.printStackTrace() println("Could not generate image!") diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt index 535a73a3..652c7ecf 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt @@ -1,19 +1,24 @@ package de.berlindroid.zekompanion.server.routers +import de.berlindroid.zekompanion.debase64 +import de.berlindroid.zekompanion.fromBinaryToRGB import de.berlindroid.zekompanion.server.ai.AI +import de.berlindroid.zekompanion.server.ai.USER_PROFILE_PICTURE_SIZE +import de.berlindroid.zekompanion.server.ext.ImageExt.toImage import de.berlindroid.zekompanion.server.user.User import de.berlindroid.zekompanion.server.user.UserRepository -import io.ktor.http.HttpStatusCode +import de.berlindroid.zekompanion.unzipit +import io.ktor.http.* import io.ktor.server.application.call import io.ktor.server.request.receiveNullable -import io.ktor.server.response.respond -import io.ktor.server.response.respondText +import io.ktor.server.response.* import io.ktor.server.routing.Route import io.ktor.server.routing.delete import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.put import java.util.* +import javax.imageio.ImageIO fun Route.adminCreateUser(users: UserRepository, ai: AI) = @@ -23,13 +28,13 @@ fun Route.adminCreateUser(users: UserRepository, ai: AI) = val uuid = UUID.randomUUID().toString() val name = ai.createUserName() val description = ai.createUserDescription(name) - val iconb64 = ai.createUserImage(name, description) + val iconB64 = ai.createUserImage(name, description) val user = User( uuid = uuid, name = name, description = description, - iconb64 = iconb64, + iconB64 = iconB64, ) val uuidAdded = users.createUser(user) @@ -109,3 +114,44 @@ fun Route.getUser(users: UserRepository) = call.respondText("Error: ${it.message}") } } + +fun Route.getUserProfileImagePng(users: UserRepository) = + get("/api/user/{UUID}/png") { + runCatching { + withParameter("UUID") { uuid -> + val user = users.getUser(uuid) + if (user != null) { + val pixels = user.iconB64.debase64().unzipit().fromBinaryToRGB() + val image = pixels.toImage(USER_PROFILE_PICTURE_SIZE, USER_PROFILE_PICTURE_SIZE) + + call.respondOutputStream(contentType = ContentType.Image.PNG) { + ImageIO.write(image, "png", this) + } + } else { + call.respondText(status = HttpStatusCode.NotFound, text = "Not Found.") + } + } + call.respondText(status = HttpStatusCode.UnprocessableEntity, text = "No UUID.") + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.getUserProfileImageBinary(users: UserRepository) = + get("/api/user/{UUID}/b64") { + runCatching { + withParameter("UUID") { uuid -> + val user = users.getUser(uuid) + if (user != null) { + call.respondText { user.iconB64 } + } else { + call.respondText(status = HttpStatusCode.NotFound, text = "Not Found.") + } + } + call.respondText(status = HttpStatusCode.UnprocessableEntity, text = "No UUID.") + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt index 83fd01ab..7753f9dc 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt @@ -1,6 +1,5 @@ package de.berlindroid.zekompanion.server.user -import de.berlindroid.zekompanion.server.ai.AI import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString @@ -13,10 +12,10 @@ private const val DB_FILENAME = "./user.db" @Serializable data class User( - val name: String? = null, - val iconb64: String? = null, - val description: String? = null, - val uuid: String? = null, + val name: String, + val iconB64: String, + val description: String, + val uuid: String, ) class UserRepository( @@ -72,11 +71,7 @@ class UserRepository( return false } - if (newUser.uuid == null) { - return false - } else { - users[index] = newUser - } + users[index] = newUser save(this) From 93ba3fa206925b4caa4c7e1a3e0b533b352ccace Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sat, 29 Jun 2024 18:11:46 +0200 Subject: [PATCH 12/26] Simpify config submission from badge to app no more special handling for strings, everything not a boolean or a number is a string. Also adding more delay before consequitive commands. The /r/n was not needed. --- .../zeapp/zeservices/ZeBadgeManager.kt | 46 ++++++++------- .../berlindroid/zekompanion/BadgeManager.kt | 2 +- zehardware/src/config.py | 56 ++++++++----------- 3 files changed, 50 insertions(+), 54 deletions(-) diff --git a/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt b/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt index ab114125..69bb63ca 100644 --- a/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt +++ b/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt @@ -10,6 +10,7 @@ import de.berlindroid.zekompanion.base64 import de.berlindroid.zekompanion.buildBadgeManager import de.berlindroid.zekompanion.toBinary import de.berlindroid.zekompanion.zipit +import kotlinx.coroutines.delay import timber.log.Timber import javax.inject.Inject @@ -85,6 +86,17 @@ class ZeBadgeManager @Inject constructor( * Return the current active configuration. */ suspend fun listConfiguration(): Result> { + badgeManager.sendPayload( + BadgePayload( + type = "config_load", + meta = "", + payload = "", + ), + ) + + badgeManager.readResponse() + delay(300) + val payload = BadgePayload( type = "config_list", meta = "", @@ -93,9 +105,14 @@ class ZeBadgeManager @Inject constructor( if (badgeManager.sendPayload(payload).isSuccess) { val response = badgeManager.readResponse() + delay(300) + if (response.isSuccess) { - val config = response.getOrDefault("") - Timber.v("Badge sent response: successfully received configuration: '${config.replace("\n", "\\n")}'.") + val config = response.getOrDefault("").replace("\r\n", "") + Timber.v( + "Badge sent response: successfully received configuration: " + + "'${config.replace("\n", "\\n")}'.", + ) val kv = mapOf( *config.split(" ").mapNotNull { @@ -136,8 +153,10 @@ class ZeBadgeManager @Inject constructor( ) if (badgeManager.sendPayload(payload).isSuccess) { + badgeManager.readResponse() + delay(300) - if (badgeManager.sendPayload( + return if (badgeManager.sendPayload( BadgePayload( type = "config_save", meta = "", @@ -145,9 +164,9 @@ class ZeBadgeManager @Inject constructor( ), ).isSuccess ) { - return Result.success(true) + Result.success(true) } else { - return Result.failure(IllegalStateException("Could not save the config to ZeBadge.")) + Result.failure(IllegalStateException("Could not save the config to ZeBadge.")) } } else { return Result.failure(NoSuchElementException("Could not update the runtime configuration on ZeBadge.")) @@ -158,29 +177,18 @@ class ZeBadgeManager @Inject constructor( } private fun pythonToKotlin(value: String): Any? = when { - value.startsWith("\"") -> - value - .replace("\"", "") - .replace(SPACE_REPLACEMENT, " ") - - - value.startsWith("\'") -> - value - .replace("\'", "") - .replace(SPACE_REPLACEMENT, " ") - - value == "None" -> null value.toIntOrNull() != null -> value.toInt() value.toFloatOrNull() != null -> value.toFloat() value == "True" -> true value == "False" -> false - else -> value + else -> value.replace(SPACE_REPLACEMENT, " ") + } private fun kotlinToPython(value: Any?): String = when (value) { null -> "None" - is String -> "\"${value.replace(" ", SPACE_REPLACEMENT)}\"" + is String -> value.replace(" ", SPACE_REPLACEMENT) is Boolean -> if (value) "True" else "False" else -> "$value" } diff --git a/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BadgeManager.kt b/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BadgeManager.kt index 72187fde..dee6fad2 100644 --- a/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BadgeManager.kt +++ b/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BadgeManager.kt @@ -16,7 +16,7 @@ data class BadgePayload( /** * Convert the payload to a format the badge understands */ - fun toBadgeCommand(): String = "${if (debug) "debug:" else ""}$type:$meta:${payload}/r/n" + fun toBadgeCommand(): String = "${if (debug) "debug:" else ""}$type:$meta:${payload}" } interface BadgeManager { diff --git a/zehardware/src/config.py b/zehardware/src/config.py index 889b3cea..e7a59877 100644 --- a/zehardware/src/config.py +++ b/zehardware/src/config.py @@ -2,18 +2,20 @@ _SPACE_REPLACEMENT_ = "$SPACE#" +def _sanatize(config): + return config.replace('\r\n', '').replace('\n', ' ') -def save_config(config, filename: str = '/ze.conf'): +def save_config(config, filename: str = 'ze.conf'): file = open(filename, 'w') if file: file.write( - fields_to_str(config) + fields_to_str(_sanatize(config)) ) def update_config(config, content: str): if content: - _execute_assignments_on_obj(config, content) + str_to_fields(config, _sanatize(content)) else: print('No content to update.') @@ -22,7 +24,7 @@ def load_config(config, filename: str = 'ze.conf') -> bool: try: file = open(filename, 'r') if file: - _execute_assignments_on_obj(config, file.read().replace('\n', ' ')) + str_to_fields(config, file.read()) return True except Exception as e: print(util.exception_to_readable(e)) @@ -33,39 +35,26 @@ def fields_to_str(obj) -> str: result = "" for field in obj: value = obj[field] - - if isinstance(value, str): - value = f"{value.replace(' ', _SPACE_REPLACEMENT_)}" + try: + value = value.replace(' ', _SPACE_REPLACEMENT_) + except Exception: + '' result += f'{field}={value} ' return result -def _execute_assignments_on_obj(obj, assignments): - assignments = assignments.split(' ') - - for assignment in assignments: - assignment = assignment.replace(_SPACE_REPLACEMENT_, ' ') - if '=' in assignment: - key, *values = assignment.split('=') - key = key.strip() - value = '='.join(values) - - if value: - value = _ensure_typed_value(value) - - obj[key] = value +def str_to_fields(obj, assignments): + l = list(filter(lambda y: len(y) == 2, map(lambda x: x.replace(_SPACE_REPLACEMENT_, ' ').replace('\n', '').split('='), assignments.split(' ')))) + read = dict(l) + for key in read: + value = read[key] + obj[key] = _ensure_typed_value(value) def _ensure_typed_value(value): - if '"' in value: - # TODO: Replace only start and end quotes. - value = str(value.replace('"', "")) - elif "'" in value: - # TODO: Replace only start and end quotes. - value = str(value.replace("'", "")) - elif value == "True": + if value == "True": value = True elif value == "False": value = False @@ -73,12 +62,12 @@ def _ensure_typed_value(value): try: value = float(value) except ValueError: - value = None + value = str(value) else: try: value = int(value) except ValueError: - value = None + value = str(value) return value @@ -103,16 +92,15 @@ def __test__(): expected = { "last_app": None, "developer_mode": True, - "wifi.ssid": "Your cat has an ssid", + "wifi.ssid": "Your cat has an ssid.", "wifi.pwd": "ask pete! !@", - "wifi.port": 1233.890 + "wifi.port": 1233.890, } cfg = fields_to_str(expected) print(cfg) - actual = {} - _execute_assignments_on_obj(actual, cfg) + str_to_fields(actual, cfg) __test_compare(actual, expected) From fbdf5eaaccd3d48acc5715e750433b67a1368754 Mon Sep 17 00:00:00 2001 From: Milos Marinkovic Date: Sun, 30 Jun 2024 01:46:47 +0200 Subject: [PATCH 13/26] Bump MockK version --- zeapp/android/build.gradle.kts | 53 ++++++++++++++++++++------------- zeapp/gradle/libs.versions.toml | 2 +- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/zeapp/android/build.gradle.kts b/zeapp/android/build.gradle.kts index d84b57c3..90490c0d 100644 --- a/zeapp/android/build.gradle.kts +++ b/zeapp/android/build.gradle.kts @@ -118,10 +118,11 @@ android { kotlinOptions { jvmTarget = "1.8" - freeCompilerArgs += listOf( - "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", - "-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi", - ) + freeCompilerArgs += + listOf( + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", + "-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi", + ) } buildFeatures { @@ -130,18 +131,21 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + kotlinCompilerExtensionVersion = + libs.versions.androidx.compose.compiler + .get() } packaging { resources { - excludes += arrayOf( - "META-INF/AL2.0", - "META-INF/LGPL2.1", - "META-INF/*.kotlin_module", - "META-INF/LICENSE.*", - "META-INF/LICENSE-notice.*", - ) + excludes += + arrayOf( + "META-INF/AL2.0", + "META-INF/LGPL2.1", + "META-INF/*.kotlin_module", + "META-INF/LICENSE.*", + "META-INF/LICENSE-notice.*", + ) } } } @@ -219,19 +223,26 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs::class).confi tasks.create("generateContributorsAsset") { val command = "git shortlog -sne --all" - val process = ProcessBuilder() - .command(command.split(" ")) - .directory(rootProject.projectDir) - .redirectOutput(Redirect.PIPE) - .redirectError(Redirect.PIPE) - .start() + val process = + ProcessBuilder() + .command(command.split(" ")) + .directory(rootProject.projectDir) + .redirectOutput(Redirect.PIPE) + .redirectError(Redirect.PIPE) + .start() process.waitFor(60, TimeUnit.SECONDS) val result = process.inputStream.bufferedReader().readText() - val contributors = result.lines() - .joinToString(separator = System.lineSeparator()) { it.substringAfter("\t") } + val contributors = + result + .lines() + .joinToString(separator = System.lineSeparator()) { it.substringAfter("\t") } - val assetDir = layout.buildDirectory.dir("generated/assets").get().asFile + val assetDir = + layout.buildDirectory + .dir("generated/assets") + .get() + .asFile assetDir.createDirectory() File(assetDir, "test.txt").writeText(contributors) } diff --git a/zeapp/gradle/libs.versions.toml b/zeapp/gradle/libs.versions.toml index 270d2776..de89e03f 100644 --- a/zeapp/gradle/libs.versions.toml +++ b/zeapp/gradle/libs.versions.toml @@ -48,7 +48,7 @@ junit4 = "4.13.2" junit4-ui = "1.6.4" ktor = "2.3.9" uiTestManifest = "1.6.4" -mockk = "1.13.8" +mockk = "1.13.11" kotlinxCoroutinesTest = "1.7.3" [libraries] From 42a42b2d1b4cc48689a9c7aafb0b47ae4ccefe87 Mon Sep 17 00:00:00 2001 From: Milos Marinkovic Date: Sun, 30 Jun 2024 01:47:01 +0200 Subject: [PATCH 14/26] Add badge config parser --- .../zeapp/zeservices/ZeBadgeConfigParser.kt | 117 ++++++++++++++++++ .../zeservices/ZeBadgeConfigParserTest.kt | 88 +++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParser.kt create mode 100644 zeapp/android/src/test/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParserTest.kt diff --git a/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParser.kt b/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParser.kt new file mode 100644 index 00000000..4f3775f2 --- /dev/null +++ b/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParser.kt @@ -0,0 +1,117 @@ +package de.berlindroid.zeapp.zeservices + +import android.util.Base64 +import java.util.UUID +import javax.inject.Inject + +private const val SPACE_ESCAPED = "\$SPACE#" + +/** + * Parses the badge configuration using the following format: + * + * ``` + * wifi_attached=False user.uuid=4d3f6ca7‑d256‑4f84‑a6c6‑099a26055d4c \ + * user.description=Edward$SPACE#Bernard,$SPACE#a$SPACE#veteran \ + * user.name=Edward$SPACE#Bernard developer_mode=True \ + * user.iconB64=eNpjYGBgUJnkqaIg6MDAAmTX/+U+WGf//399OwNjYfv/gk1AQ==" + * ``` + */ +class ZeBadgeConfigParser + @Inject + constructor() { + fun parse(configString: String): ParseResult { + val configMap = + configString + .split("\\s+".toRegex()) + .map { it.split("=", limit = 2) } + .filter { it.size == 2 } + .associate { it[0] to it[1] } + + val userId = parseUserId(configMap) + val userName = parseUserName(configMap) + val userDescription = parseUserDescription(configMap) + val userProfilePhoto = parseUserProfilePhoto(configMap) + val userInfo = + if ( + userId != null && userName != null && userDescription != null && userProfilePhoto != null + ) { + UserInfo(userId, userName, userDescription, userProfilePhoto) + } else { + null + } + + return ParseResult( + userInfo = userInfo, + isWiFiAttached = parseWiFiAttached(configMap), + isDeveloperMode = parseDeveloperMode(configMap), + ) + } + + private fun parseWiFiAttached(configMap: Map): Boolean = configMap["wifi_attached"]?.toBoolean() ?: false + + private fun parseDeveloperMode(configMap: Map): Boolean = configMap["developer_mode"]?.toBoolean() ?: false + + private fun parseUserId(configMap: Map): UUID? = configMap["user.uuid"]?.let { UUID.fromString(it) } + + private fun parseUserProfilePhoto(configMap: Map): ByteArray? = + configMap["user.iconB64"]?.let { Base64.decode(it, Base64.DEFAULT) } + + private fun parseUserName(configMap: Map): String? = configMap["user.name"]?.unescape() + + private fun parseUserDescription(configMap: Map): String? = configMap["user.description"]?.unescape() + + private fun String.unescape() = replace(SPACE_ESCAPED, " ") + } + +data class ParseResult( + val userInfo: UserInfo?, + val isWiFiAttached: Boolean, + val isDeveloperMode: Boolean, +) { + fun flatten(): Map { + val map = mutableMapOf() + userInfo?.flatten()?.forEach { (key, value) -> + map["user.$key"] = value + } + map["isWiFiAttached"] = isWiFiAttached + map["isDeveloperMode"] = isDeveloperMode + return map + } +} + +data class UserInfo( + val id: UUID, + val name: String, + val description: String, + val profilePhoto: ByteArray, +) { + fun flatten() = + mapOf( + "id" to id.toString(), + "name" to name, + "description" to description, + "profilePhoto" to Base64.encode(profilePhoto, Base64.DEFAULT), + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UserInfo + + if (id != other.id) return false + if (name != other.name) return false + if (description != other.description) return false + if (!profilePhoto.contentEquals(other.profilePhoto)) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + profilePhoto.contentHashCode() + return result + } +} diff --git a/zeapp/android/src/test/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParserTest.kt b/zeapp/android/src/test/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParserTest.kt new file mode 100644 index 00000000..228000ca --- /dev/null +++ b/zeapp/android/src/test/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParserTest.kt @@ -0,0 +1,88 @@ +package de.berlindroid.zeapp.zeservices + +import android.util.Base64 +import assertk.assertFailure +import assertk.assertions.messageContains +import io.mockk.every +import io.mockk.mockkStatic +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.util.UUID + +class ZeBadgeConfigParserTest { + private val parser = ZeBadgeConfigParser() + + @Before + fun setup() { + mockkStatic(Base64::class) + every { Base64.decode(any(), any()) } returns byteArrayOf(1, 2, 3, 4) + } + + @Test + fun `parse valid config string`() { + val configString = + "wifi_attached=False user.uuid=4d3f6ca7-d256-4f84-a6c6-099a26055d4c " + + "user.description=Edward\$SPACE#Bernard,\$SPACE#a\$SPACE#veteran " + + "user.name=Edward\$SPACE#Bernard developer_mode=True " + + "user.iconB64=eNpjYGBgUJnkqaIg6MDAAmTX/+U+WGd//399OwNjYfv/gk1AQ==" + + val result = parser.parse(configString) + + assertNotNull(result.userInfo) + assertEquals(UUID.fromString("4d3f6ca7-d256-4f84-a6c6-099a26055d4c"), result.userInfo?.id) + assertEquals("Edward Bernard", result.userInfo?.name) + assertEquals("Edward Bernard, a veteran", result.userInfo?.description) + assertFalse(result.isWiFiAttached) + assertTrue(result.isDeveloperMode) + assertNotNull(result.userInfo?.profilePhoto) + assertArrayEquals(byteArrayOf(1, 2, 3, 4), result.userInfo?.profilePhoto) + } + + @Test + fun `parse config string with missing user info`() { + val configString = "wifi_attached=True developer_mode=False" + + val result = parser.parse(configString) + + assertNull(result.userInfo) + assertTrue(result.isWiFiAttached) + assertFalse(result.isDeveloperMode) + } + + @Test + fun `parse config string with partial user info`() { + val configString = + "user.uuid=4d3f6ca7-d256-4f84-a6c6-099a26055d4c " + + "user.name=John\$SPACE#Doe wifi_attached=True" + + val result = parser.parse(configString) + + assertNull(result.userInfo) + assertTrue(result.isWiFiAttached) + assertFalse(result.isDeveloperMode) + } + + @Test + fun `parse config string with invalid UUID`() { + val configString = + "user.uuid=invalid-uuid user.name=John\$SPACE#Doe " + + "user.description=Test user.iconB64=eNpjYGBg" + + assertFailure { parser.parse(configString) } + .messageContains("invalid-uuid") + } + + @Test + fun `parse config string with multiple spaces between key-value pairs`() { + val configString = + "wifi_attached=True developer_mode=True " + + "user.uuid=4d3f6ca7-d256-4f84-a6c6-099a26055d4c" + + val result = parser.parse(configString) + + assertNull(result.userInfo) + assertTrue(result.isWiFiAttached) + assertTrue(result.isDeveloperMode) + } +} From 8984a3655d6699fe9bcd5de48b2a77cedbd975ff Mon Sep 17 00:00:00 2001 From: Milos Marinkovic Date: Sun, 30 Jun 2024 01:47:14 +0200 Subject: [PATCH 15/26] Update badge manager to use the latest parser --- .../zeapp/zeservices/ZeBadgeManager.kt | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt b/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt index 69bb63ca..e5444b56 100644 --- a/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt +++ b/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt @@ -18,6 +18,7 @@ private const val SPACE_REPLACEMENT = "\$SPACE#" class ZeBadgeManager @Inject constructor( @ApplicationContext private val context: Context, + private val badgeConfigParser: ZeBadgeConfigParser, ) { private val badgeManager = buildBadgeManager(Environment(context)) @@ -75,10 +76,10 @@ class ZeBadgeManager @Inject constructor( payload = "", ) - if (badgeManager.sendPayload(payload).isSuccess) { - return badgeManager.readResponse() + return if (badgeManager.sendPayload(payload).isSuccess) { + badgeManager.readResponse() } else { - return Result.failure(NoSuchElementException()) + Result.failure(NoSuchElementException()) } } @@ -114,19 +115,10 @@ class ZeBadgeManager @Inject constructor( "'${config.replace("\n", "\\n")}'.", ) - val kv = mapOf( - *config.split(" ").mapNotNull { - if ("=" in it) { - val (key, value) = it.split("=") - val typedValue = pythonToKotlin(value) - key to typedValue - } else { - Timber.v("Config '$it' is malformed, ignoring it.") - null - } - }.toTypedArray(), - ) - return Result.success(kv) + val parseResult = badgeConfigParser.parse(config) + Timber.v("Badge config parsed: $parseResult") + + return Result.success(parseResult.flatten()) } return Result.failure(IllegalStateException("Could not read response.")) } else { @@ -176,16 +168,6 @@ class ZeBadgeManager @Inject constructor( fun isConnected(): Boolean = badgeManager.isConnected() } -private fun pythonToKotlin(value: String): Any? = when { - value == "None" -> null - value.toIntOrNull() != null -> value.toInt() - value.toFloatOrNull() != null -> value.toFloat() - value == "True" -> true - value == "False" -> false - else -> value.replace(SPACE_REPLACEMENT, " ") - -} - private fun kotlinToPython(value: Any?): String = when (value) { null -> "None" is String -> value.replace(" ", SPACE_REPLACEMENT) From f58822b5984ac0390e6f87715411fac72249715f Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 01:49:01 +0200 Subject: [PATCH 16/26] Resize images and store pngs --- .gitignore | 1 + zeapp/profiles/.ignore | 1 + .../berlindroid/zekompanion/server/ai/AI.kt | 49 +++++++++++++++++-- .../zekompanion/server/ai/Dalle.kt | 20 ++------ .../zekompanion/server/routers/UserRouter.kt | 33 +++++-------- .../zekompanion/server/user/UserRepository.kt | 9 ++-- 6 files changed, 64 insertions(+), 49 deletions(-) create mode 100644 zeapp/profiles/.ignore diff --git a/.gitignore b/.gitignore index e6b63eac..24bbd7ce 100644 --- a/.gitignore +++ b/.gitignore @@ -562,3 +562,4 @@ obj/ /zeapp/versions.properties *.db +zeapp/profiles/*png diff --git a/zeapp/profiles/.ignore b/zeapp/profiles/.ignore new file mode 100644 index 00000000..a20b29a7 --- /dev/null +++ b/zeapp/profiles/.ignore @@ -0,0 +1 @@ +please diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt index a002fed0..3c3815a9 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt @@ -1,7 +1,31 @@ package de.berlindroid.zekompanion.server.ai -import java.lang.IndexOutOfBoundsException +import de.berlindroid.zekompanion.base64 +import de.berlindroid.zekompanion.ditherFloydSteinberg +import de.berlindroid.zekompanion.server.ext.ImageExt.toPixels +import de.berlindroid.zekompanion.toBinary +import de.berlindroid.zekompanion.zipit +import java.awt.Image +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.File +import java.nio.ByteBuffer +import javax.imageio.ImageIO +const val USER_PROFILE_PICTURE_SIZE = 32 + +private fun rescaleImage( + image: BufferedImage, + targetWidth: Int = USER_PROFILE_PICTURE_SIZE, + targetHeight: Int = USER_PROFILE_PICTURE_SIZE, +): BufferedImage { + val scaledImage = image.getScaledInstance(targetWidth, targetHeight, Image.SCALE_SMOOTH) + val destinationImage = BufferedImage(targetWidth, targetHeight, image.type) + val graphics = destinationImage.createGraphics() + graphics.drawImage(scaledImage, 0, 0, null) + graphics.dispose() + return destinationImage +} class AI( private val gemini: Gemini = Gemini(), @@ -51,8 +75,23 @@ class AI( suspend fun createUserDescription(name: String): String = gemini.getDescription(name) - suspend fun createUserImage(name: String, description: String): String = dale.requestImageGeneration( - name = name, - description = description, - ) ?: "" + suspend fun createUserProfileImages(uuid: String, name: String, description: String): String? { + val image = dale.requestImageGeneration( + name = name, + description = description, + ) + + return if (image != null) { + ImageIO.write(image, "png", File("./profiles/${uuid}.png")) + + rescaleImage(image) + .toPixels() + .ditherFloydSteinberg(USER_PROFILE_PICTURE_SIZE, USER_PROFILE_PICTURE_SIZE) + .toBinary() + .zipit() + .base64() + } else { + null + } + } } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt index 8f105c3f..dfb24a20 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt @@ -1,7 +1,5 @@ package de.berlindroid.zekompanion.server.ai -import de.berlindroid.zekompanion.* -import de.berlindroid.zekompanion.server.ext.ImageExt.toPixels import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -12,6 +10,7 @@ import retrofit2.converter.kotlinx.serialization.asConverterFactory import retrofit2.http.Body import retrofit2.http.Header import retrofit2.http.POST +import java.awt.image.BufferedImage import java.net.URL import java.util.concurrent.TimeUnit import javax.imageio.ImageIO @@ -49,8 +48,6 @@ interface OpenAIService { private const val TIMEOUT: Long = 90 -const val USER_PROFILE_PICTURE_SIZE = 32 - class Dalle( private val json: Json = Json { ignoreUnknownKeys = true }, private val service: OpenAIService = Retrofit.Builder() @@ -76,7 +73,7 @@ class Dalle( suspend fun requestImageGeneration( name: String, description: String, - ): String? { + ): BufferedImage? { try { val maybeImages = service.generateImage( request = ImageRequest( @@ -91,18 +88,7 @@ class Dalle( val location = maybeImages.data.firstOrNull() ?: return null println("Avatar of '$name' generated at ${location.url}.") - val image = ImageIO.read(URL(location.url)) - val width = image.width - val height = image.height - val b64 = image - .toPixels() - .resize(width, height, USER_PROFILE_PICTURE_SIZE, USER_PROFILE_PICTURE_SIZE) - .ditherFloydSteinberg(USER_PROFILE_PICTURE_SIZE, USER_PROFILE_PICTURE_SIZE) - .toBinary() - .zipit() - .base64() - - return b64 + return ImageIO.read(URL(location.url)) } catch (e: Exception) { e.printStackTrace() println("Could not generate image!") diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt index 652c7ecf..45ae48e5 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt @@ -1,24 +1,15 @@ package de.berlindroid.zekompanion.server.routers -import de.berlindroid.zekompanion.debase64 -import de.berlindroid.zekompanion.fromBinaryToRGB import de.berlindroid.zekompanion.server.ai.AI -import de.berlindroid.zekompanion.server.ai.USER_PROFILE_PICTURE_SIZE -import de.berlindroid.zekompanion.server.ext.ImageExt.toImage import de.berlindroid.zekompanion.server.user.User import de.berlindroid.zekompanion.server.user.UserRepository -import de.berlindroid.zekompanion.unzipit import io.ktor.http.* -import io.ktor.server.application.call -import io.ktor.server.request.receiveNullable +import io.ktor.server.application.* +import io.ktor.server.request.* import io.ktor.server.response.* -import io.ktor.server.routing.Route -import io.ktor.server.routing.delete -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.put +import io.ktor.server.routing.* +import java.io.File import java.util.* -import javax.imageio.ImageIO fun Route.adminCreateUser(users: UserRepository, ai: AI) = @@ -28,13 +19,14 @@ fun Route.adminCreateUser(users: UserRepository, ai: AI) = val uuid = UUID.randomUUID().toString() val name = ai.createUserName() val description = ai.createUserDescription(name) - val iconB64 = ai.createUserImage(name, description) + + val b64 = ai.createUserProfileImages(uuid, name, description) val user = User( uuid = uuid, name = name, description = description, - iconB64 = iconB64, + profileB64 = b64, ) val uuidAdded = users.createUser(user) @@ -121,12 +113,9 @@ fun Route.getUserProfileImagePng(users: UserRepository) = withParameter("UUID") { uuid -> val user = users.getUser(uuid) if (user != null) { - val pixels = user.iconB64.debase64().unzipit().fromBinaryToRGB() - val image = pixels.toImage(USER_PROFILE_PICTURE_SIZE, USER_PROFILE_PICTURE_SIZE) - - call.respondOutputStream(contentType = ContentType.Image.PNG) { - ImageIO.write(image, "png", this) - } + call.respondFile( + File("./profiles/${uuid}.png"), + ) } else { call.respondText(status = HttpStatusCode.NotFound, text = "Not Found.") } @@ -144,7 +133,7 @@ fun Route.getUserProfileImageBinary(users: UserRepository) = withParameter("UUID") { uuid -> val user = users.getUser(uuid) if (user != null) { - call.respondText { user.iconB64 } + call.respondText { user.profileB64 ?: "" } } else { call.respondText(status = HttpStatusCode.NotFound, text = "Not Found.") } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt index 7753f9dc..b55dbb77 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt @@ -12,10 +12,10 @@ private const val DB_FILENAME = "./user.db" @Serializable data class User( + val uuid: String, val name: String, - val iconB64: String, val description: String, - val uuid: String, + val profileB64: String?, ) class UserRepository( @@ -49,12 +49,11 @@ class UserRepository( return null } - val uuid = UUID.randomUUID().toString() - users.add(user.copy(uuid = uuid)) + users.add(user) save(this) - return uuid + return user.uuid } fun getUser(uuid: String): User? { From 86770dfffb375cfe6330486ef8eb4166001091e8 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 01:50:51 +0200 Subject: [PATCH 17/26] update parsing of server for flashing --- zehardware/src/config.py | 2 +- zehardware/zefirmware/zeflash.py | 74 +++++++++++++++++++------------- 2 files changed, 44 insertions(+), 32 deletions(-) mode change 100644 => 100755 zehardware/zefirmware/zeflash.py diff --git a/zehardware/src/config.py b/zehardware/src/config.py index e7a59877..bf608cdd 100644 --- a/zehardware/src/config.py +++ b/zehardware/src/config.py @@ -46,7 +46,7 @@ def fields_to_str(obj) -> str: def str_to_fields(obj, assignments): - l = list(filter(lambda y: len(y) == 2, map(lambda x: x.replace(_SPACE_REPLACEMENT_, ' ').replace('\n', '').split('='), assignments.split(' ')))) + l = list(filter(lambda y: len(y) == 2, map(lambda x: x.replace(_SPACE_REPLACEMENT_, ' ').replace('\n', '').split('=',1), assignments.split(' ')))) read = dict(l) for key in read: value = read[key] diff --git a/zehardware/zefirmware/zeflash.py b/zehardware/zefirmware/zeflash.py old mode 100644 new mode 100755 index 15ce7a04..3c2496bf --- a/zehardware/zefirmware/zeflash.py +++ b/zehardware/zefirmware/zeflash.py @@ -73,7 +73,7 @@ def colorize(text): return result -def request_new_user_id(): +def request_new_user(): assert SERVER_TOKEN, f"SERVER_AUTH not set: '{SERVER_TOKEN}'." users = requests.get( @@ -86,31 +86,34 @@ def request_new_user_id(): print(f"Found {len(users)} users. Adding another one.") - new_uuid = requests.post( + new_user = requests.post( url=f"{BASE_URL}/api/user", headers={ 'Content-Type': 'application/json', 'ZeAuth': SERVER_TOKEN }, - json={ - } + json={} ) - if new_uuid.ok: - new_uuid = new_uuid.json()['uuid'] - print(f".. user '{colorize(new_uuid)}' created.") + if new_user.ok: + user = new_user.json() + print(f".. user '{colorize(user['name'])}' created.") else: print(f".. couldn't create user: '{colorize(new_uuid.text)}'.") - return new_uuid # server shenanigans + return user # server shenanigans def find_mount_point(name): - mounted = subprocess.run(["mount"], capture_output=True, text=True, check=True).stdout - - if name in mounted: - return list(map(lambda y: y.split()[2], filter(lambda x: name in x, mounted.splitlines())))[0] + mount = subprocess.run(["mount"], capture_output=True, check=True) + if mount.returncode != 0: + print( + f'"mount" returned error code {mount.errorcode}.\n\nCheck outputs\nstd:{mount.stdout}\nerr:{mount.stderr}\n') + return None + output = mount.stdout.decode() + mount.stderr.decode() + if name in output: + return list(map(lambda y: y.split()[2], filter(lambda x: name in x, output.splitlines())))[0] else: return None @@ -138,10 +141,14 @@ def nuke(): path = find_mount_point('RPI') if not path: - print(f"Please put badge in flash mode.\n{colorize('restart')} ZeBadge and hold {colorize('boot / usr')} button.") + print( + f"Please put badge in flash mode.\n" + f"{colorize('restart')} ZeBadge and " + f"hold {colorize('boot / usr')} button." + ) while not find_mount_point('RPI'): - print('.', end='') + print('.', flush=True, end='') time.sleep(0.1) print() path = find_mount_point('RPI') @@ -153,7 +160,7 @@ def nuke(): time.sleep(0.3) while not find_mount_point('RPI'): - print('.', end='') + print('.', flush=True, end='') time.sleep(0.1) print() @@ -172,7 +179,7 @@ def flash(): print(f"Please put badge in flash mode.\n{colorize('restart')} and hold {colorize('boot / usr')} button.") while not find_mount_point('RPI'): - print('.', end='') + print('.', flush=True, end='') time.sleep(0.1) print() @@ -183,7 +190,7 @@ def flash(): complete = False def wait_on_exception(): - print('x', end='') + print('x', flush=True, end='') time.sleep(1) while not complete: @@ -196,7 +203,7 @@ def wait_on_exception(): wait_on_exception() while not find_mount_point('CIRC'): - print('.', end='') + print('.', flush=True, end='') time.sleep(0.1) print() @@ -205,7 +212,7 @@ def wait_on_exception(): def copy_code(): circuit = find_mount_point('CIRC') while not circuit: - print('.', end='') + print('.', flush=True, end='') time.sleep(0.1) circuit = find_mount_point('CIRC') @@ -217,9 +224,9 @@ def copy_code(): for src in source_files: try: shutil.copy('../src/' + src, circuit) - print('.', end='') + print('.', flush=True, end='') except IsADirectoryError: - print(':', end='') + print(':', flush=True, end='') print() print(colorize("copy bitmap resources")) @@ -228,26 +235,29 @@ def copy_code(): try: if res.endswith('bmp'): shutil.copy('../resources/' + res, circuit) - print('.', end='') + print('.', flush=True, end='') else: - print(',', end='') + print(',', flush=True, end='') except IsADirectoryError: - print(':', end='') + print(':', flush=True, end='') print() -def inject_user_id(uuid): +def inject_user(user): circuit = find_mount_point('CIRC') while not circuit: - print('.', end='') + print('.', flush=True, end='') time.sleep(0.1) circuit = find_mount_point('CIRC') - print(colorize("injecting user id")) - open(circuit + "/ze.conf", "w+").writelines([ - f'uuid={uuid} ', - ]) + user_config = " ".join(list(map(lambda x: f'user.{x}={user[x].replace(' ', "$SPACE#")}', user))) + + print(f"{colorize("injecting user")}: {user_config}") + + open(circuit + "/ze.conf", "w+").write( + user_config + ) if __name__ == '__main__': @@ -259,9 +269,11 @@ def inject_user_id(uuid): exit(-1) nuke() + flash() + copy_code() - inject_user_id(request_new_user_id()) + inject_user(request_new_user()) print(colorize("!! DONE !!")) From ea17b492861c43176025f423a420ed59555a9335 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 03:58:03 +0200 Subject: [PATCH 18/26] updating ai generations --- .../berlindroid/zekompanion/server/ai/AI.kt | 1 + .../zekompanion/server/ai/Dalle.kt | 7 ++- .../zekompanion/server/ai/Gemini.kt | 55 ++++++++++++++----- .../zekompanion/server/routers/UserRouter.kt | 2 + .../zekompanion/server/user/UserRepository.kt | 2 +- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt index 3c3815a9..0d903150 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt @@ -74,6 +74,7 @@ class AI( } suspend fun createUserDescription(name: String): String = gemini.getDescription(name) + suspend fun createUserChatPhrase(name: String, description: String): String = gemini.getChatPhrase(name, description) suspend fun createUserProfileImages(uuid: String, name: String, description: String): String? { val image = dale.requestImageGeneration( diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt index dfb24a20..fb4078fe 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt @@ -78,9 +78,10 @@ class Dalle( val maybeImages = service.generateImage( request = ImageRequest( model = "dall-e-3", - prompt = "Please create a digital picture of \"${name}\", a player character of a black and white pixelated game. " + - "The picture should show them in action doing their favorite thing, it should be isometric. " + - "$name can be described as follows: '${description}'.", + prompt = "Please create a profile picture for a fantastical school book showing \"${name}\". " + + "The picture should be a decorated wooden frame hanging on a blank wall " + + "and show $name in a representative pose, doing their favorite thing. " + + "$description.", ), authorization = "Bearer $token", ) diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt index e6dffe1b..99679378 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt @@ -13,6 +13,7 @@ private const val AI_TOKEN_ENV = "AI_AUTH_TOKEN" @Serializable data class PromptBody( + val systemInstruction: Content, val contents: List, ) { @Serializable @@ -73,30 +74,55 @@ class Gemini( ) { suspend fun getDescription(name: String): String { - val prompt = PromptBody( - contents = listOf( - PromptBody.Content( - parts = listOf( - PromptBody.Content.Part( - text = "You are a dungeons and dragons dungeon master, building a dnd session at the Droidcon in Berlin in 2024. You'll answer with a 100 words description of characters backgrounds.", - ), - PromptBody.Content.Part( - text = "A new player joins: \"${name}\". Please give me a background description of this character playing at the Droidcon in Berlin 2024.", - ), - ), - ), + return geminiIt( + systemInstruction = "You are a dungeons and dragons dungeon master, assembling a new dnd campaign " + + "at the Droidcon in Berlin conference in 2024. You'll answer following questions with a " + + "description of a character's background story. Their all Android developers, either " + + "excited new comers or old hands with years of experience. Please keep it brief, " + + "interesting and quirky.", + prompts = listOf( + "Please give \"${name}\" a background story of their character.", ), ) + } + suspend fun getChatPhrase(name: String, description: String): String { + return geminiIt( + systemInstruction = "You are a one word wonder. Answer the following prompts with one word.", + prompts = listOf( + "Hey, can you create a one word catchphrase for $name who is described as $description.", + ), + ) + } + + private suspend fun geminiIt(prompts: List, systemInstruction: String = ""): String { try { val response = service.prompt( token, - prompt, + PromptBody( + systemInstruction = + PromptBody.Content( + parts = listOf( + PromptBody.Content.Part( + text = systemInstruction, + ), + ), + ), + contents = listOf( + PromptBody.Content( + parts = prompts.map { + PromptBody.Content.Part( + text = it, + ) + }, + ), + ), + ), ) return response.candidates.joinToString(separator = ",") { candidate -> candidate.content.parts.joinToString { part -> - part.text + part.text.replace(Regex("[_*#\n]"), "").trim() } } } catch (e: Exception) { @@ -104,6 +130,5 @@ class Gemini( print("Couldn't gemini!") return "" } - } } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt index 45ae48e5..3b5e8398 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt @@ -19,6 +19,7 @@ fun Route.adminCreateUser(users: UserRepository, ai: AI) = val uuid = UUID.randomUUID().toString() val name = ai.createUserName() val description = ai.createUserDescription(name) + val chatPhrase = ai.createUserChatPhrase(name, description) val b64 = ai.createUserProfileImages(uuid, name, description) @@ -27,6 +28,7 @@ fun Route.adminCreateUser(users: UserRepository, ai: AI) = name = name, description = description, profileB64 = b64, + chatPhrase = chatPhrase, ) val uuidAdded = users.createUser(user) diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt index b55dbb77..e8f45dfa 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt @@ -6,7 +6,6 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.File import java.io.FileNotFoundException -import java.util.UUID private const val DB_FILENAME = "./user.db" @@ -16,6 +15,7 @@ data class User( val name: String, val description: String, val profileB64: String?, + val chatPhrase: String?, ) class UserRepository( From d88add254dfcd0827a44cfbb486a733bbc3ebea2 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 04:27:14 +0200 Subject: [PATCH 19/26] fix flashing minor bugs --- zehardware/zefirmware/zeflash.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zehardware/zefirmware/zeflash.py b/zehardware/zefirmware/zeflash.py index 3c2496bf..3f1a3b69 100755 --- a/zehardware/zefirmware/zeflash.py +++ b/zehardware/zefirmware/zeflash.py @@ -98,17 +98,18 @@ def request_new_user(): if new_user.ok: user = new_user.json() print(f".. user '{colorize(user['name'])}' created.") + return user else: - print(f".. couldn't create user: '{colorize(new_uuid.text)}'.") + print(f".. couldn't create user: '{colorize(new_user.json())}'.") - return user # server shenanigans + return None def find_mount_point(name): mount = subprocess.run(["mount"], capture_output=True, check=True) if mount.returncode != 0: print( - f'"mount" returned error code {mount.errorcode}.\n\nCheck outputs\nstd:{mount.stdout}\nerr:{mount.stderr}\n') + f'"mount" returned error code {mount.returncode}.\n\nCheck outputs\nstd:{mount.stdout}\nerr:{mount.stderr}\n') return None output = mount.stdout.decode() + mount.stderr.decode() From 0f44b47274cd273cb8dd81b5ce14cec4a1faa9e6 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 17:28:20 +0200 Subject: [PATCH 20/26] adding non ssl version of server --- .../de/berlindroid/zekompanion/server/Main.kt | 75 +++++++++++++------ .../server/routers/ZePassRouter.kt | 28 ++++--- .../server/zepass/ZePassRepository.kt | 14 ++-- 3 files changed, 73 insertions(+), 44 deletions(-) diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt index 1cf97678..06b73941 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt @@ -11,17 +11,62 @@ import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.http.content.* import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.response.* import io.ktor.server.routing.* -import io.ktor.server.tomcat.Tomcat +import io.ktor.server.tomcat.* import java.io.File import java.security.KeyStore -private const val LOCAL_HTTP_PORT = 8000 -private const val LOCAL_TLS_PORT = 8443 private const val SSL_PASSWORD_ENV = "SSL_CERTIFICATE_PASSWORD" private const val KEYSTORE_RESOURCE_FILE = "/tmp/keystore.jks" -fun main(args: Array) { +fun main() { + val users = UserRepository.load() + val zepass = ZePassRepository.load() + + embeddedBadgeServer(users, zepass) + .start(wait = false) + + embeddedWebServer(users, zepass) + .start(wait = true) +} + +private fun embeddedBadgeServer( + users: UserRepository, + zepass: ZePassRepository, +): TomcatApplicationEngine { + return embeddedServer( + Tomcat, + environment = applicationEngineEnvironment { + connector { + port = 1337 + } + + module { + install(ContentNegotiation) { + json() + } + + routing { + get("/") { + call.respondText("yes") + } + + postPost(zepass, users) + getPosts(zepass) + + getUser(users) + getUserProfileImageBinary(users) + } + } + }, + ) +} + +private fun embeddedWebServer( + users: UserRepository, + zepass: ZePassRepository, +): TomcatApplicationEngine { val keyPassword = try { System.getenv(SSL_PASSWORD_ENV) } catch (e: Exception) { @@ -29,14 +74,9 @@ fun main(args: Array) { } val keyStore: KeyStore? = loadKeyStore(keyPassword) - val serverPort = extractServerPort(args, keyStore) - println("Serving on port $serverPort.") - - val users = UserRepository.load() val ai = AI() - val zepass = ZePassRepository.load() - embeddedServer( + return embeddedServer( Tomcat, environment = applicationEngineEnvironment { injectTLSIfNeeded(keyStore, keyPassword) @@ -73,7 +113,7 @@ fun main(args: Array) { } } }, - ).start(wait = true) + ) } private fun ApplicationEngineEnvironmentBuilder.injectTLSIfNeeded(keyStore: KeyStore?, keyPassword: String?) { @@ -89,19 +129,6 @@ private fun ApplicationEngineEnvironmentBuilder.injectTLSIfNeeded(keyStore: KeyS } } -private fun extractServerPort(args: Array, keyStore: KeyStore?): Int { - val serverPort = if (args.isNotEmpty()) { - args.first().toInt() - } else { - if (keyStore != null) { - LOCAL_TLS_PORT - } else { - LOCAL_HTTP_PORT - } - } - return serverPort -} - private fun loadKeyStore(keyPassword: String?): KeyStore? { var keyStore: KeyStore? = null diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt index 9e029725..335305f8 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt @@ -3,24 +3,28 @@ package de.berlindroid.zekompanion.server.routers import de.berlindroid.zekompanion.server.user.UserRepository import de.berlindroid.zekompanion.server.zepass.Post import de.berlindroid.zekompanion.server.zepass.ZePassRepository -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.call -import io.ktor.server.request.receive -import io.ktor.server.response.respond -import io.ktor.server.response.respondText -import io.ktor.server.routing.Route -import io.ktor.server.routing.get -import io.ktor.server.routing.post +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.util.* fun Route.postPost(zepass: ZePassRepository, users: UserRepository) = post("/api/zepass") { runCatching { - val post = call.receive() - - val user = users.getUser(post.posterUUID) + val uuid = call.receive() + val user = users.getUser(uuid) if (user != null) { - val postUUID = zepass.newPost(post) + val postUUID = UUID.randomUUID().toString() + zepass.newPost( + Post( + uuid = postUUID, + posterUUID = uuid, + message = user.chatPhrase ?: "", + ), + ) call.respond(status = HttpStatusCode.OK, postUUID) } else { diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt index 28c2072e..f9c123fd 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt @@ -11,7 +11,7 @@ private const val POSTS_FILENAME = "./zepass.db" @Serializable data class Post( - val uuid: String? = null, + val uuid: String, val posterUUID: String, val message: String, ) @@ -25,6 +25,7 @@ class ZePassRepository private constructor( posts = Json.decodeFromString(File(POSTS_FILENAME).readText()), ) } catch (notFound: FileNotFoundException) { + println("Couldn't find '$POSTS_FILENAME'.") ZePassRepository() } @@ -34,14 +35,11 @@ class ZePassRepository private constructor( } fun newPost(post: Post): String { - val uuid = UUID.randomUUID().toString() - posts.add( - post.copy(uuid = uuid), - ) + val repo = load() + posts.add(post) + save(repo) - save(this) - - return uuid + return post.uuid } fun getPosts(): List { From 2ad29f119999bd782667af2583080edc4adab2b3 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 17:28:39 +0200 Subject: [PATCH 21/26] update zebadge to connect to server, hopefully. --- zehardware/src/app_fetch.py | 46 +++++++++++----------- zehardware/src/app_store_and_show.py | 4 +- zehardware/src/wifi.py | 35 +++++++++++++---- zehardware/src/zeos.py | 58 ++++++++++++++++------------ zehardware/zefirmware/zeflash.py | 7 ++++ 5 files changed, 94 insertions(+), 56 deletions(-) diff --git a/zehardware/src/app_fetch.py b/zehardware/src/app_fetch.py index 7b0924b7..59aa93d4 100644 --- a/zehardware/src/app_fetch.py +++ b/zehardware/src/app_fetch.py @@ -1,5 +1,5 @@ -import zeos import wifi +import zeos from message import Message @@ -10,43 +10,45 @@ def __init__(self, os: zeos.ZeBadgeOs): def run(self): self.subscription_ids += [ - self.os.subscribe(wifi.MessageKey.SCAN, _scanned), self.os.subscribe(wifi.MessageKey.CONNECT_RESULT, _connected), self.os.subscribe(wifi.MessageKey.GET_RESULT, _getted), + self.os.subscribe( + zeos.MessageKey.BUTTON_CHANGED, + lambda os, message: self._buttons_changed(message.value) + ), ] - self.os.messages.append(Message(zeos.MessageKey.INFO, 'scanning wifi')) - self.os.messages.append(Message(wifi.MessageKey.SCAN_RESULT)) + def _buttons_changed(self, changed): + if 'up' in changed and not changed['up']: + config = { + 'ssid': self.os.config['wifi.ssid'], + 'pwd': self.os.config['wifi.pwd'] + } + self.os.messages.append(Message(zeos.MessageKey.INFO, f'Trying to connect: {config}')) + self.os.messages.append( + Message( + wifi.MessageKey.CONNECT, + config + ) + ) def unrun(self): for subscription in self.subscription_ids: self.os.unsubscribe(subscription) -def _scanned(os: zeos.ZeBadgeOs, message): - if message.value: - os.messages.append( - Message(zeos.MessageKey.INFO, f"Found '{"', '".join(message.value)}' WiFis\nConnecting to wifi ...")) - os.messages.append(Message(wifi.MessageKey.CONNECT, { - 'ssid': os.config.wifi.ssid, - 'pwd': os.config.wifi.pwd - })) - else: - os.messages.append(Message(zeos.MessageKey.INFO, 'Could not scan wifi')) - - def _connected(os: zeos.ZeBadgeOs, message): if message.value: - os.messages.append(Message(zeos.MessageKey.INFO, 'GET sample data ...')) + os.messages.append(Message(zeos.MessageKey.INFO, 'Connected, GETing sample data ...')) os.messages.append(Message(wifi.MessageKey.GET, { - 'ip': os.config.wifi.ip, - 'url': os.config.wifi.url, - 'host': os.config.wifi.host, - 'port': os.config.wifi.port, + 'ip': os.config['wifi.ip'], + 'url': os.config['wifi.url'], + 'host': os.config['wifi.host'], + 'port': os.config['wifi.port'], })) else: os.messages.append(Message(zeos.MessageKey.ERROR, 'Could not connect')) def _getted(os: zeos.ZeBadgeOs, message): - os.messages.append(Message(zeos.MessageKey.INFO, f'data received: {message.value.body}')) + os.messages.append(Message(zeos.MessageKey.INFO, f'data received: {message.value}')) diff --git a/zehardware/src/app_store_and_show.py b/zehardware/src/app_store_and_show.py index f0245383..8c1707fb 100644 --- a/zehardware/src/app_store_and_show.py +++ b/zehardware/src/app_store_and_show.py @@ -1,5 +1,5 @@ -import zeos import ui +import zeos from message import Message @@ -24,7 +24,7 @@ def unrun(self): self.os.unsubscribe(subscription_id) def _buttons_changed(self, changed): - self.os.messages.append(Message("INFO", f"{changed}")) + self.os.messages.append(Message(zeos.MessageKey.INFO, f"{changed}")) if 'up' in changed and not changed['up']: self._load_previous() if 'down' in changed and not changed['down']: diff --git a/zehardware/src/wifi.py b/zehardware/src/wifi.py index 52f8698b..017182b8 100644 --- a/zehardware/src/wifi.py +++ b/zehardware/src/wifi.py @@ -1,7 +1,9 @@ +import time + import board import busio import microcontroller -import time + from message import Message @@ -78,6 +80,9 @@ def scan(self) -> map[str:list[Network]] | None: def connect(self, ssid: str, pwd: str) -> bool: available_networks = self.scan() + if not available_networks: + available_networks = self.scan() + if available_networks and ssid in available_networks: found = sorted(available_networks[ssid], key=lambda x: x.strength)[-1] @@ -139,18 +144,34 @@ def init(os) -> bool: wifi = ZeWifi() - if wifi.scan(): + scan = wifi.scan() + if not scan: + scan = wifi.scan() + + if scan: os.subscribe(MessageKey.SCAN, lambda _, message: os.messages.append( Message(MessageKey.SCAN_RESULT, wifi.scan()))) os.subscribe(MessageKey.CONNECT, lambda _, message: os.messages.append( - Message(MessageKey.CONNECT_RESULT, wifi.connect(message.value['ssid'], message.value['pwd'])))) + Message( + MessageKey.CONNECT_RESULT, + wifi.connect( + message.value['ssid'], + message.value['pwd'] + ) + ) + )) os.subscribe(MessageKey.GET, lambda _, message: os.messages.append( - Message(MessageKey.GET_RESULT, - wifi.http_get(message.value['ip'], message.value['url'], message.value['host'], - message.value['port']) - ) + Message( + MessageKey.GET_RESULT, + wifi.http_get( + message.value['ip'], + message.value['url'], + message.value['host'], + message.value['port'] + ) + ) )) return True else: diff --git a/zehardware/src/zeos.py b/zehardware/src/zeos.py index e2ec9b7b..57b3a231 100644 --- a/zehardware/src/zeos.py +++ b/zehardware/src/zeos.py @@ -1,26 +1,23 @@ -import board -import gc -import serial -import sys +import os as systemos import time import traceback -import ui -import usb_cdc - -import os as systemos +import board +import usb_cdc from digitalio import DigitalInOut from digitalio import Direction from digitalio import Pull -from message import Message -from config import save_config -from config import load_config -from config import update_config -from config import fields_to_str +import serial +import ui +from app_developer_idle_clicker import DeveloperIdleClickerApp from app_fetch import FetchApp from app_store_and_show import StoreAndShowApp -from app_developer_idle_clicker import DeveloperIdleClickerApp +from config import fields_to_str +from config import load_config +from config import save_config +from config import update_config +from message import Message class MessageKey: @@ -60,21 +57,21 @@ def __init__(self): self.buttons = SystemButtons() self.tasks.append(_update_system_buttons) + self._reset_subscribers() + self._subscribe_to_system_buttons() # add defaults self.config = {} load_config(self.config) + self._init_interfaces() + # applications self._app_a = None self._app_b = None self._app_c = None self._init_apps() - self._reset_subscribers() - self._init_interfaces() - self._subscribe_to_system_buttons() - self.system_subscribers = self.subscribers.copy() def _reset_subscribers(self): @@ -148,7 +145,7 @@ def run(self): subscriber = subscriber_ids[subscriber_id] subscriber(self, message) else: - print(f'?', end='') + print(f'?({message.topic})', end='') self.led_on = not self.led_on self.led.value = self.led_on @@ -179,23 +176,34 @@ def _init_interfaces(self): import keyboard keyboard.init(self) - self.config["keyboard_attached"] = has_keyboard + self.config["keyboard.attached"] = has_keyboard else: + self.config["keyboard.attached"] = False + print("... no i2c found, trying wifi") import wifi if not wifi.init(self): print("... no wifi found.") - self.config["wifi_attached"] = False + self.config["wifi.attached"] = False else: - self.config["wifi_attached"] = True + print("... wifi !!!") + self.config["wifi.attached"] = True - self.config["developer_mode"] = not (usb_cdc.data is None) + self.config["developer.mode"] = not (usb_cdc.data is None) def _init_apps(self): self._app_a = StoreAndShowApp(self) - self._app_b = FetchApp(self) - self._app_c = DeveloperIdleClickerApp(self) + + if self.config["wifi.attached"]: + self._app_b = None + self._app_c = FetchApp(self) + elif self.config['keyboard.attached']: + self._app_b = None + self._app_c = DeveloperIdleClickerApp(self) + else: + self._app_b = None + self._app_c = DeveloperIdleClickerApp(self) self._start_app(self._app_a) diff --git a/zehardware/zefirmware/zeflash.py b/zehardware/zefirmware/zeflash.py index 3f1a3b69..cdeaa8fc 100755 --- a/zehardware/zefirmware/zeflash.py +++ b/zehardware/zefirmware/zeflash.py @@ -253,6 +253,13 @@ def inject_user(user): circuit = find_mount_point('CIRC') user_config = " ".join(list(map(lambda x: f'user.{x}={user[x].replace(' ', "$SPACE#")}', user))) + user_config += \ + f"wifi.ssid=droidcon24 " \ + f"wifi.pwd=helloDrdoicon24 " \ + f"wifi.ip=35.208.138.148 " \ + f"wifi.port=1337 " \ + f"wifi.url=/api/zepass " \ + f"wifi.host=zebadge.app " print(f"{colorize("injecting user")}: {user_config}") From 0cdab4c16184260a35980580060070771ff0100c Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 21:51:09 +0200 Subject: [PATCH 22/26] ZePass: Fetch posted messages --- zehardware/src/app_fetch.py | 54 -------------- zehardware/src/app_zepass.py | 132 +++++++++++++++++++++++++++++++++++ zehardware/src/wifi.py | 69 ++++++++++++------ zehardware/src/zeos.py | 4 +- 4 files changed, 180 insertions(+), 79 deletions(-) delete mode 100644 zehardware/src/app_fetch.py create mode 100644 zehardware/src/app_zepass.py diff --git a/zehardware/src/app_fetch.py b/zehardware/src/app_fetch.py deleted file mode 100644 index 59aa93d4..00000000 --- a/zehardware/src/app_fetch.py +++ /dev/null @@ -1,54 +0,0 @@ -import wifi -import zeos -from message import Message - - -class FetchApp: - def __init__(self, os: zeos.ZeBadgeOs): - self.os = os - self.subscription_ids = [] - - def run(self): - self.subscription_ids += [ - self.os.subscribe(wifi.MessageKey.CONNECT_RESULT, _connected), - self.os.subscribe(wifi.MessageKey.GET_RESULT, _getted), - self.os.subscribe( - zeos.MessageKey.BUTTON_CHANGED, - lambda os, message: self._buttons_changed(message.value) - ), - ] - - def _buttons_changed(self, changed): - if 'up' in changed and not changed['up']: - config = { - 'ssid': self.os.config['wifi.ssid'], - 'pwd': self.os.config['wifi.pwd'] - } - self.os.messages.append(Message(zeos.MessageKey.INFO, f'Trying to connect: {config}')) - self.os.messages.append( - Message( - wifi.MessageKey.CONNECT, - config - ) - ) - - def unrun(self): - for subscription in self.subscription_ids: - self.os.unsubscribe(subscription) - - -def _connected(os: zeos.ZeBadgeOs, message): - if message.value: - os.messages.append(Message(zeos.MessageKey.INFO, 'Connected, GETing sample data ...')) - os.messages.append(Message(wifi.MessageKey.GET, { - 'ip': os.config['wifi.ip'], - 'url': os.config['wifi.url'], - 'host': os.config['wifi.host'], - 'port': os.config['wifi.port'], - })) - else: - os.messages.append(Message(zeos.MessageKey.ERROR, 'Could not connect')) - - -def _getted(os: zeos.ZeBadgeOs, message): - os.messages.append(Message(zeos.MessageKey.INFO, f'data received: {message.value}')) diff --git a/zehardware/src/app_zepass.py b/zehardware/src/app_zepass.py new file mode 100644 index 00000000..a7a04d7c --- /dev/null +++ b/zehardware/src/app_zepass.py @@ -0,0 +1,132 @@ +import json + +import displayio +import terminalio +from adafruit_display_text import label + +import wifi +import zeos +from message import Message +from ui import MessageKey as UIKeys + + +class ZePassApp: + def __init__(self, os: zeos.ZeBadgeOs): + self.os = os + self.subscription_ids = [] + self.method = "" + + def run(self): + self.subscription_ids += [ + self.os.subscribe(wifi.MessageKey.CONNECT_RESULT, self._connected), + self.os.subscribe(wifi.MessageKey.GET_RESULT, self._response_received), + self.os.subscribe(wifi.MessageKey.POST_RESULT, self._response_received), + + self.os.subscribe( + zeos.MessageKey.BUTTON_CHANGED, + lambda os, message: self._buttons_changed(message.value) + ), + ] + + def unrun(self): + for subscription in self.subscription_ids: + self.os.unsubscribe(subscription) + + def _buttons_changed(self, changed): + if 'up' in changed and not changed['up']: + self._fetch_all_posts() + + if 'down' in changed and not changed['down']: + self._create_new_post() + + def _fetch_all_posts(self): + config = { + 'ssid': self.os.config['wifi.ssid'], + 'pwd': self.os.config['wifi.pwd'] + } + self.method = "GET" + self.os.messages.append(Message(zeos.MessageKey.INFO, f'Connecting to wifi {config} ')) + self.os.messages.append( + Message( + wifi.MessageKey.CONNECT, + config + ) + ) + + def _connected(self, os: zeos.ZeBadgeOs, message): + if message.value: + config = { + 'ip': os.config['wifi.ip'], + 'url': os.config['wifi.url'], + 'host': os.config['wifi.host'], + 'port': os.config['wifi.port'], + } + + if self.method == "GET": + os.messages.append(Message(zeos.MessageKey.INFO, f'Connected, GETing passes {config}.')) + os.messages.append(Message(wifi.MessageKey.GET, config)) + elif self.method == "POST": + config['body'] = os.config['user.uuid'] + os.messages.append(Message(zeos.MessageKey.INFO, f'Connected, POSTing {config}.')) + os.messages.append(Message(wifi.MessageKey.POST, config)) + else: + os.messages.append( + Message( + zeos.MessageKey.ERROR, + f"Zepass method {self.method} not understood." + ) + ) + self.method = '' + + else: + os.messages.append(Message(zeos.MessageKey.ERROR, 'Could not connect')) + + def _create_new_post(self): + self.method = "POST" + + config = { + 'ssid': self.os.config['wifi.ssid'], + 'pwd': self.os.config['wifi.pwd'] + } + self.os.messages.append(Message(zeos.MessageKey.INFO, f'Trying to connect: {config}')) + self.os.messages.append( + Message( + wifi.MessageKey.CONNECT, + config + ) + ) + + def _response_received(self, os: zeos.ZeBadgeOs, message): + if self.method == "GET": + self._update_all_posts( + message.value.body + ) + + elif self.method == "POST": + self.method = None + self._fetch_all_posts() + + def _update_all_posts(self, raw_posts): + group = displayio.Group() + font = terminalio.FONT + posts = json.loads(raw_posts) + + for index, post in enumerate(posts): + # TODO: ADD FANCY USER LOGO HERE + post_area = label.Label( + font, + text=post['message'], + background_color=0x000000, + color=0xFFFFFF, + ) + post_area.x = 5 + 0 + post_area.y = 5 + index * 10 + group.append(post_area) + print(f"posting {post['message']}") + + self.os.messages.append( + Message( + UIKeys.SHOW_GROUP, + group + ) + ) diff --git a/zehardware/src/wifi.py b/zehardware/src/wifi.py index 017182b8..969b212f 100644 --- a/zehardware/src/wifi.py +++ b/zehardware/src/wifi.py @@ -14,6 +14,8 @@ class MessageKey: CONNECT_RESULT = "CONNECT_RESULT" GET = "GET" GET_RESULT = "GET_RESULT" + POST = "POST" + POST_RESULT = "POST_RESULT" class Network: @@ -33,7 +35,7 @@ def __init__(self, status, headers, body): self.body = body def __str__(self): - return f"{self.status}:{self.headers}= {self.body}" + return f"{self.status} {self.headers}\n{self.body}" class ZeWifi: @@ -98,7 +100,8 @@ def connect(self, ssid: str, pwd: str) -> bool: print(f"Network '{ssid}' was not found. These are available: '{"' ".join(available_networks.keys())}'.") return False - def http_get(self, ip: str, url: str, host: str = "", port: int = 80) -> HttpResponse | None: + def _http_method(self, method: str, ip: str, url: str, host: str = "", port: int = 80, + body: str = "") -> HttpResponse | None: response = "" while len(response) == 0 or "STATUS:3" not in response.decode(): # connect to ip @@ -113,7 +116,17 @@ def http_get(self, ip: str, url: str, host: str = "", port: int = 80) -> HttpRes response = self.uart.read() # create http payload - payload = f"GET {url} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: ZeWeb/0.1337.0\r\nAccept: */*\r\n\r\n" + payload = (f"{method} {url} HTTP/1.1\r\n" + + f"Host: {ip}:{port}\r\n" + + f"User-Agent: ZeBadge/0.1337.0\r\n" + + f"Accept: */*\r\n") + + if len(body) > 0: + payload += (f"Content-type: application/json\r\n" + + f"Content-Length: {len(body)}\r\n") + + payload += f"\r\n{body}" + self.uart.write(f'AT+CIPSEND={len(payload)}\r\n') self.uart.read() @@ -131,6 +144,12 @@ def http_get(self, ip: str, url: str, host: str = "", port: int = 80) -> HttpRes return response + def http_get(self, ip: str, url: str, host: str = "", port: int = 80) -> HttpResponse | None: + return self._http_method("GET", ip, url, host, port) + + def http_post(self, ip: str, url: str, host: str = "", port: int = 80, body: str = "") -> HttpResponse | None: + return self._http_method("POST", ip, url, host, port, body) + wifi = None @@ -173,13 +192,26 @@ def init(os) -> bool: ) ) )) + + os.subscribe(MessageKey.POST, lambda _, message: os.messages.append( + Message( + MessageKey.POST_RESULT, + wifi.http_post( + message.value['ip'], + message.value['url'], + message.value['host'], + message.value['port'], + message.value['body'], + ) + ) + )) return True else: return False def _parse_response(response: str) -> HttpResponse | None: - parts = response.replace('\r\n', '\n').splitlines()[5:-1] + parts = response.replace('\r\n', '\n').splitlines()[5:] status_code = parts.pop(0) if status_code.startswith('+IPD'): @@ -190,27 +222,18 @@ def _parse_response(response: str) -> HttpResponse | None: status_code = 444 headers = {} - body = "" for index, header in enumerate(parts): - key, *values = header.split(":") - if len(key) == 0: - continue - - if len(values) > 1: - value = ":".join(values) - elif len(values) == 1: - value = values[0] - else: - value = "" - - # found the end? - if key.startswith('+IPD'): - # yes - body = value + try: + key, value = header.split(":", 1) + except ValueError: break - else: - # nope, keep iterating through headers - headers[key] = value.strip() + key = key.strip() + value = value.strip() + + # nope, keep iterating through headers + headers[key] = value.strip() + + body = parts[-1] response = HttpResponse(status_code, headers, body) return response diff --git a/zehardware/src/zeos.py b/zehardware/src/zeos.py index 57b3a231..c2a58cdd 100644 --- a/zehardware/src/zeos.py +++ b/zehardware/src/zeos.py @@ -11,7 +11,7 @@ import serial import ui from app_developer_idle_clicker import DeveloperIdleClickerApp -from app_fetch import FetchApp +from app_zepass import ZePassApp from app_store_and_show import StoreAndShowApp from config import fields_to_str from config import load_config @@ -197,7 +197,7 @@ def _init_apps(self): if self.config["wifi.attached"]: self._app_b = None - self._app_c = FetchApp(self) + self._app_c = ZePassApp(self) elif self.config['keyboard.attached']: self._app_b = None self._app_c = DeveloperIdleClickerApp(self) From 9a2c6025da2afc16d8f43b13149d5a57e8defcae Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 22:14:53 +0200 Subject: [PATCH 23/26] Server: ZePass optimize response for zebadge --- .../de/berlindroid/zekompanion/server/Main.kt | 5 +---- .../zekompanion/server/routers/ZePassRouter.kt | 10 ++++++++++ .../server/zepass/ZePassRepository.kt | 17 ++++++++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt index 06b73941..2415b9fd 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt @@ -53,10 +53,7 @@ private fun embeddedBadgeServer( } postPost(zepass, users) - getPosts(zepass) - - getUser(users) - getUserProfileImageBinary(users) + getOptimizedPosts(zepass, users) } } }, diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt index 335305f8..d6441aa2 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt @@ -45,3 +45,13 @@ fun Route.getPosts(zepass: ZePassRepository) = call.respondText("Error: ${it.message}") } } + +fun Route.getOptimizedPosts(zepass: ZePassRepository, users: UserRepository) = + get("/api/zepass") { + runCatching { + call.respond(zepass.getOptimizedPosts(users)) + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt index f9c123fd..71e92030 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt @@ -1,11 +1,11 @@ package de.berlindroid.zekompanion.server.zepass +import de.berlindroid.zekompanion.server.user.UserRepository import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.File import java.io.FileNotFoundException -import java.util.UUID private const val POSTS_FILENAME = "./zepass.db" @@ -16,6 +16,12 @@ data class Post( val message: String, ) +@Serializable +data class OptimizedPosts( + val message: String, + val profileB64: String?, +) + class ZePassRepository private constructor( private val posts: MutableList = mutableListOf(), ) { @@ -45,4 +51,13 @@ class ZePassRepository private constructor( fun getPosts(): List { return posts.toList() } + + fun getOptimizedPosts(users: UserRepository): List { + return posts.map { + OptimizedPosts( + message = it.message, + profileB64 = users.getUser(it.posterUUID)?.profileB64, + ) + } + } } From d6cf9bb998ad2d3374baae6d104bc28dbf6fc929 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 22:16:46 +0200 Subject: [PATCH 24/26] fix: remove stupid file handling save only on change to file, otherwise in memory db for ssl and badge server. --- .../berlindroid/zekompanion/server/zepass/ZePassRepository.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt index 71e92030..caae3dac 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt @@ -41,9 +41,8 @@ class ZePassRepository private constructor( } fun newPost(post: Post): String { - val repo = load() posts.add(post) - save(repo) + save(this) return post.uuid } From 5910c8ed77ac088988731cc40415a9881cca195b Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 22:58:03 +0200 Subject: [PATCH 25/26] remove annyoing debug message --- zehardware/src/app_store_and_show.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zehardware/src/app_store_and_show.py b/zehardware/src/app_store_and_show.py index 8c1707fb..866b589a 100644 --- a/zehardware/src/app_store_and_show.py +++ b/zehardware/src/app_store_and_show.py @@ -24,7 +24,6 @@ def unrun(self): self.os.unsubscribe(subscription_id) def _buttons_changed(self, changed): - self.os.messages.append(Message(zeos.MessageKey.INFO, f"{changed}")) if 'up' in changed and not changed['up']: self._load_previous() if 'down' in changed and not changed['down']: From 5858f136b3763714062ae763e35ed71a41ab72fe Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Sun, 30 Jun 2024 22:58:24 +0200 Subject: [PATCH 26/26] ZEPASS!!!! First time full roundtrip from badge to server to badge to server and back. Happiness in black and white. --- zehardware/src/app_zepass.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/zehardware/src/app_zepass.py b/zehardware/src/app_zepass.py index a7a04d7c..0ec7a334 100644 --- a/zehardware/src/app_zepass.py +++ b/zehardware/src/app_zepass.py @@ -4,6 +4,7 @@ import terminalio from adafruit_display_text import label +import ui import wifi import zeos from message import Message @@ -63,7 +64,7 @@ def _connected(self, os: zeos.ZeBadgeOs, message): } if self.method == "GET": - os.messages.append(Message(zeos.MessageKey.INFO, f'Connected, GETing passes {config}.')) + os.messages.append(Message(zeos.MessageKey.INFO, f'Connected, GETing posts {config}.')) os.messages.append(Message(wifi.MessageKey.GET, config)) elif self.method == "POST": config['body'] = os.config['user.uuid'] @@ -109,20 +110,39 @@ def _response_received(self, os: zeos.ZeBadgeOs, message): def _update_all_posts(self, raw_posts): group = displayio.Group() font = terminalio.FONT - posts = json.loads(raw_posts) + + try: + posts = json.loads(raw_posts) + except ValueError: + if "+IPD" in raw_posts: + posts = json.loads(raw_posts.split(':', 1)[1]) + else: + print(f'Could not parse response: {raw_posts}') + return for index, post in enumerate(posts): # TODO: ADD FANCY USER LOGO HERE post_area = label.Label( font, + scale=2, text=post['message'], background_color=0x000000, color=0xFFFFFF, ) - post_area.x = 5 + 0 - post_area.y = 5 + index * 10 + x_offset =(index % 2) * 16 + post_area.x = x_offset + 40 + post_area.y = 16 + index * 32 + group.append(post_area) - print(f"posting {post['message']}") + if 'profileB64' in post and post['profileB64']: + profile = post['profileB64'] + bitmap, palette = ui.decode_serialized_bitmap(profile, 32, 32) + profile_grid = displayio.TileGrid(bitmap, pixel_shader=palette) + profile_grid.x = x_offset + profile_grid.y = index * 32 + group.append(profile_grid) + else: + print(f'no profile for message {index}.') self.os.messages.append( Message(