Services¶
Die Services liegen in src/app/core/. Sie haben keine Oberfläche, sondern liefern Daten an die Seiten. Die App verschlüsselt keine
Nachrichten (das macht die Antenne); für das Entsperren der Antenne nutzt sie nur WebCrypto (crypto.subtle, crypto.getRandomValues)
und ein eigenes X25519 – es gibt keine Krypto-Bibliothek.
AntennaPort¶
Datei: src/app/core/antenna/antenna-port.ts. Ein Singleton (providedIn: 'root'). Die Antenne aus Sicht der App: Verbindung, Zustand,
Befehle und Ereignisse. Details in Antenne.
| Name | Typ | Beschreibung |
|---|---|---|
state |
Signal<AntennaState> |
disconnected, unprovisioned, locked oder unlocked. |
connecting |
Signal<boolean> |
true, solange connect() läuft. |
connected |
Signal<boolean> |
Verbunden, in jedem Gerätezustand. |
device |
Signal<DeviceInfo \| null> |
Gerätedaten aus sys.hello (Name, Chip, deviceId, Firmware, Protokoll). |
identity |
Signal<{ nodeId, name } \| null> |
Eigene Kennung, solange entsperrt. |
limits |
Signal<DeviceLimits> |
Grenzen des Geräts; Standard 2048 / 32 / 83 / 8, bis sys.hello geantwortet hat. |
error |
Signal<AntennaErrorCode \| null> |
Letzter Fehler der Verbindung oder des Entsperrens. |
events |
Observable<DeviceEvent> |
Typisierte Ereignisse: sys.boot, sec.state, msg.received, msg.status, contact.state. |
connect() |
Promise<void> |
Leitung öffnen, sys.hello, ein gesperrtes Gerät von selbst entsperren, Heartbeat starten. |
disconnect() |
Promise<void> |
Verbindung beenden. |
provision(name) |
Promise<boolean> |
Frisches Gerät einrichten (sec.provision). |
unlock() |
Promise<boolean> |
Gesperrtes Gerät entsperren (sec.unlock). |
lock() |
Promise<void> |
Sofort sperren (sec.lock). |
request(cmd, args?) |
Promise<JsonObject> |
Ein Befehl der Host-API. Wirft DeviceError mit dem Fehlercode des Geräts. |
SerialClient¶
Datei: src/app/core/antenna/serial-client.ts. Eine Klasse (kein Service). Spricht JSON Lines: setzt Zeilen zusammen, vergibt ids, hält höchstens
4 Anfragen in Flight, prüft die Zeilenlänge (2048 Bytes) und die Zeitlimits (2 s, 5 s für sec.*, pair.complete, contact.delete), wiederholt
sys.hello alle 500 ms und sendet alle 5 s sys.ping. Er kennt weder USB noch einzelne Befehle.
SerialTransport¶
Datei: src/app/core/antenna/serial-transport.ts. Abstrakte Klasse und DI-Token: die Leitung zur Antenne (Text hinein, Text heraus).
MockSerialTransport hängt das simulierte Gerät MockDevice an; UsbSerialTransport spricht auf Android über das native USB-Plugin UsbSerial.
UsbSerialTransport¶
Datei: src/app/core/antenna/usb-serial.transport.ts. Der Transport auf Android: Er sucht über das native Plugin UsbSerial das erste USB-Gerät mit
passendem Serial-Treiber, öffnet es mit 115200 Baud und 8N1 und dekodiert die ankommenden Base64-Daten mit einem TextDecoder im Stream-Modus, damit
Mehrbyte-Zeichen über Paketgrenzen hinweg ganz bleiben. Die Reject-Codes des Plugins übersetzt er in TransportError:
PERMISSION_DENIED → permission-denied, OPEN_FAILED → open-failed, NO_DRIVER → no-device. Die Ereignisse detached und error melden die
Verbindung als geschlossen. Details in Antenne.
KeystoreService¶
Datei: src/app/core/keystore/keystore.service.ts. Abstrakte Klasse und DI-Token: das Entsperr-Geheimnis K_phone.
| Name | Typ | Beschreibung |
|---|---|---|
unlockToken(deviceId) |
Promise<Uint8Array> |
HMAC-SHA256(K_phone, "og-unlock-v1" ‖ device_id_bytes), 32 Bytes, pro Gerät. |
createUnlockEnvelope(session, deviceId) |
Promise<{ appEphPub, wrappedToken }> |
Neuer Einmal-Schlüssel und das eingepackte Token (60 Bytes) für sec.provision und sec.unlock. |
BrowserKeystoreService hält einen zufälligen Schlüssel im Arbeitsspeicher; AndroidKeystoreService rechnet den HMAC über das native
Plugin OffgridKeystore (T06, Java-Klasse OffgridKeystorePlugin). X25519 steckt in x25519.ts, HKDF und AES-GCM in unlock-wrap.ts (WebCrypto).
MessagingService¶
Datei: src/app/core/messaging/messaging.service.ts. Ein Singleton (providedIn: 'root'). Senden und Empfangen über die Host-API; die App
verschlüsselt nichts. Details in Nachrichten.
| Name | Typ | Beschreibung |
|---|---|---|
maxContentBytes |
Signal<number> |
Textlimit des Geräts (limits.maxTextBytes), unverändert; 83. |
contacts |
Signal<ContactListItem[]> |
Die Kontakte mit letzter Nachricht, Anzahl der ungelesenen Nachrichten und Signal-Level; sortiert nach der letzten Nachricht (neueste zuerst), Kontakte ohne Nachricht stehen nach ihrer Platznummer. Die Liste lädt bei jeder Datenänderung neu (DataChangedService.version). |
unreadCount(contactId) |
Signal<number> |
Anzahl der ungelesenen Nachrichten eines Kontakts; das Signal bleibt bestehen und liefert den neuen Wert. |
reactions |
Observable<ReactionEvent> |
Eingehende Reaktionen (werden nicht gespeichert). |
sendText(contactId, text) |
Promise<void> |
Teilt den Text an maxContentBytes() Bytes; jedes Stück ist eine eigene Nachricht. |
sendLocation(contactId, fix) |
Promise<void> |
Standort als Nachricht (type: "geo"). |
sendReaction(contactId, target, emoji) |
Promise<boolean> |
Reaktion senden; emoji = null nimmt sie zurück. |
retry(messageId) |
Promise<void> |
Gescheiterte Nachricht noch einmal senden. |
cancel(messageId) |
Promise<void> |
Nachricht abbrechen (msg.cancel). |
markRead(contactId) |
Promise<void> |
Eingehende Nachrichten eines Kontakts als gelesen markieren. |
LocationService¶
Datei: src/app/core/location/location.service.ts. Ein Singleton (providedIn: 'root'). Bestimmt den eigenen Standort
und hält den letzten Fix. Details in Standort.
| Name | Typ | Beschreibung |
|---|---|---|
myFix |
Signal<GeoFix \| null> |
Der eigene Standort, falls schon bestimmt. |
locating |
Signal<boolean> |
true, solange locate() läuft. |
error |
Signal<LocationErrorCode \| null> |
Der Fehler der letzten Ortung. |
locate() |
Promise<GeoFix \| null> |
Berechtigung holen und den GPS fragen; im Browser nach 600 ms der Demo-Fix. |
isFresh(fix, now) |
boolean |
true, wenn der Fix höchstens 10 Minuten alt ist. |
HandshakeService¶
Datei: src/app/core/handshake/handshake.service.ts. Ein Singleton (providedIn: 'root'). Das Pairing macht das Gerät; der Dienst
spiegelt die Kontakte. Details in Kontakte & Handshake.
| Name | Typ | Beschreibung |
|---|---|---|
qrText |
Signal<string \| null> |
Text für den eigenen QR-Code. |
qrExpiresAt |
Signal<number \| null> |
Ablaufzeit des Codes (ms seit 1970). |
error |
Signal<HandshakeErrorCode \| null> |
Letzter Fehler. |
completedContactId |
Signal<number \| null> |
Platz des gerade hinzugefügten Kontakts. |
begin() |
Promise<string \| null> |
pair.begin: liefert den QR-Text. |
cancel() |
Promise<void> |
pair.cancel. |
complete(qrText, options?) |
Promise<Contact \| null> |
pair.complete: legt den Kontakt (pending, mit fingerprint) an. |
syncContacts() |
Promise<void> |
Alle Kontakte vom Gerät holen (contact.list) und lokal speichern. Läuft bei jedem Entsperren. |
refreshContact(contactId) |
Promise<Contact \| null> |
Einen Kontakt holen (contact.get). |
rename(contactId, name) |
Promise<boolean> |
contact.rename (1–24 Bytes). |
remove(contactId) |
Promise<boolean> |
contact.delete und lokal samt Verlauf. |
clearCompleted() |
void |
completedContactId zurücksetzen. |
DatabaseService¶
src/app/core/data/database.service.ts definiert die abstrakte Klasse. Sie dient zugleich als DI-Token, damit die App
je nach Plattform dieselbe Schnittstelle verwendet.
| Name | Typ | Beschreibung |
|---|---|---|
open() |
Promise<void> |
Datenbank öffnen und offene Migrationen ausführen. |
replaceAll(data) |
Promise<void> |
Alle Nutzdaten ersetzen; die Einstellungen bleiben erhalten. |
deleteAllUserData() |
Promise<void> |
Kontakte, Nachrichten und Koordinaten löschen; die Einstellungen bleiben erhalten. |
SqliteDatabaseService¶
Die native Implementierung nutzt @capacitor-community/sqlite. Sie öffnet die unverschlüsselte Datenbank offgrid,
aktiviert Fremdschlüssel und führt die Migrationen aus.
| Name | Typ | Beschreibung |
|---|---|---|
open() |
Promise<void> |
Verbindung öffnen, PRAGMA foreign_keys = ON setzen und Migrationen ausführen. |
replaceAll(data) |
Promise<void> |
Nutzdaten in einer Transaktion ersetzen. |
deleteAllUserData() |
Promise<void> |
Nutzdaten in einer Transaktion löschen. |
query(statement, values?) |
Promise<Record<string, unknown>[]> |
Abfrage mit gebundenen Parametern ausführen. |
run(statement, values?) |
Promise<void> |
Schreibstatement mit gebundenen Parametern ausführen. |
MemoryDatabaseService¶
src/app/core/data/memory/memory-database.service.ts ist die Browser-Implementierung. Ihre Daten liegen in Maps der
Memory-Repositories. open() hat im Browser nichts zu öffnen.
| Name | Typ | Beschreibung |
|---|---|---|
open() |
Promise<void> |
No-op im Browser. |
replaceAll(data) |
Promise<void> |
Nutzdaten ersetzen und display_name speichern. |
deleteAllUserData() |
Promise<void> |
Nutzdaten löschen; die Einstellungen bleiben erhalten. |
seedDemo() |
Promise<void> |
Browser mit den Demo-Daten aus memory/seed.ts füllen (DEMO_CONTACTS, dieselben Kontakte kennt das Mock-Gerät). |
SettingsService¶
Datei: src/app/core/settings/settings.service.ts. Ein Singleton (providedIn: 'root'). Die gespeicherten Werte
kommen aus dem SettingsRepository; die Signale sind der schnelle Lesezugriff.
| Name | Typ | Beschreibung |
|---|---|---|
displayName |
Signal<string> |
Anzeigename; standardmässig leer. |
installId |
Signal<string> |
Install-Id; beim ersten Laden mit crypto.randomUUID() erzeugt und gespeichert. |
bgReceive |
Signal<boolean> |
Hintergrundempfang; Standard true. |
vibrate |
Signal<boolean> |
Vibration; Standard false. |
onboardingDone |
Signal<boolean> |
Onboarding abgeschlossen; Standard false. |
load() |
Promise<void> |
Einstellungen laden, fehlende install_id erzeugen und Speicherwerte übernehmen. |
setDisplayName(value) |
Promise<void> |
Anzeigename speichern. |
setBgReceive(value) |
Promise<void> |
Hintergrundempfang als 1 oder 0 speichern. |
setVibrate(value) |
Promise<void> |
Vibration als 1 oder 0 speichern. |
completeOnboarding() |
Promise<void> |
onboarding_done auf 1 setzen. |
DataChangedService¶
Datei: src/app/core/data/data-changed.service.ts. Ein Singleton (providedIn: 'root'). Der Service meldet
Änderungen an Kontakten, Nachrichten oder ganzen Datenbeständen.
| Name | Typ | Beschreibung |
|---|---|---|
version |
Signal<number> |
Version der Datenänderungen; beginnt bei 0. |
notify() |
void |
Version um eins erhöhen. |
AppStartService¶
Datei: src/app/core/app-start.service.ts. Ein Singleton (providedIn: 'root'). AppComponent ruft init() beim
Erstellen der Wurzelkomponente auf.
| Name | Typ | Beschreibung |
|---|---|---|
init() |
Promise<void> |
Datenbank und Einstellungen starten, die Antenne im Hintergrund verbinden und auf Android Statusleiste und Splashscreen vorbereiten. |
Die Schritte laufen in dieser Reihenfolge:
DatabaseService.open()öffnet die Datenbank und führt die Migrationen aus.- Im Browser ruft der Dienst
MemoryDatabaseService.seedDemo()auf. Der Schritt entfällt beilocalStorage.getItem('og.noseed') === '1'. SettingsService.load()lädt die Einstellungen und legt eine fehlende Install-Id an.MessagingServiceundHandshakeServicewerden erzeugt (sie hören auf die Antenne), dann startetAntennaPort.connect()im Hintergrund. Der Aufruf wird nicht abgewartet: Fehler stehen inAntennaPort.error. Ein gesperrtes Gerät entsperrt der Port selbst.- Nur nativ setzt der Dienst den Statusleistenstil auf
Darkund die definierte Hintergrundfarbe. - Nur nativ blendet der Dienst den Splashscreen aus.
Die Startrouten-Berechnung gehört nicht dazu. Fehler beim Start werden mit log.error protokolliert.
Repositories¶
Die Repositories sind abstrakte Klassen in src/app/core/data/. Jede Klasse dient als DI-Token. Für jede gibt es eine
SQLite-Implementierung in derselben Datei und eine Memory-Implementierung unter src/app/core/data/memory/.
ContactRepository¶
Die Kontakte gehören der Antenne; die App spiegelt sie (ohne Schlüssel).
| Name | Rückgabe | Beschreibung |
|---|---|---|
all() |
Promise<Contact[]> |
Alle Kontakte nach Platz (contactId). |
byId(contactId) |
Promise<Contact \| null> |
Ein Kontakt nach Platz. |
upsert(contact) |
Promise<void> |
Kontakt einfügen oder aktualisieren; der Verlauf des Kontakts bleibt dabei erhalten. |
updateName(contactId, name) |
Promise<void> |
Namen ändern. |
updateLink(contactId, rssi, snr) |
Promise<void> |
Signalwerte des letzten empfangenen Pakets. |
setState(contactId, state) |
Promise<void> |
Zustand ändern (pending → active). |
delete(contactId) |
Promise<void> |
Kontakt löschen; Nachrichten und Koordinaten des Kontakts fallen mit weg. |
count() |
Promise<number> |
Anzahl der Kontakte. |
MessageRepository¶
| Name | Rückgabe | Beschreibung |
|---|---|---|
byId(id) |
Promise<Message \| null> |
Eine Nachricht nach Id. |
forContact(contactId) |
Promise<Message[]> |
Nachrichten eines Kontakts, älteste zuerst. |
lastPerContact() |
Promise<Map<number, Message>> |
Letzte Nachricht je Kontakt. |
unreadPerContact() |
Promise<Map<number, number>> |
Anzahl ungelesener eingehender Nachrichten je Kontakt. |
queued() |
Promise<Message[]> |
Ausgehende Nachrichten, die noch nicht beim Gerät sind (Status 0, kein msgRef), älteste zuerst. |
byMsgRef(msgRef) |
Promise<Message \| null> |
Offene ausgehende Nachricht mit diesem Handle des Geräts. |
byCtr(contactId, direction, ctr) |
Promise<Message \| null> |
Nachricht nach Kontakt, Richtung und Zähler. |
insert(message) |
Promise<void> |
Nachricht einfügen. |
setStatus(id, status) |
Promise<void> |
Status ändern und updated_at setzen. |
assignRef(id, ctr, msgRef, status) |
Promise<void> |
Zähler und Handle aus der Antwort auf msg.send eintragen. |
requeue(id) |
Promise<void> |
Gescheiterte Nachricht ohne Zähler und Handle wieder in die Warteschlange legen. |
failPending() |
Promise<number> |
Nach einem Neustart des Geräts alles als gescheitert markieren, was dort wartete. |
markRead(contactId, at) |
Promise<void> |
Alle ungelesenen eingehenden Nachrichten des Kontakts als gelesen markieren. |
count() |
Promise<number> |
Anzahl der Nachrichten. |
CoordinateRepository¶
| Name | Rückgabe | Beschreibung |
|---|---|---|
insert(coordinate) |
Promise<void> |
Koordinate einfügen. |
latestForMessage(messageId) |
Promise<Coordinate \| null> |
Neueste Koordinate der Nachricht. |
latestForMessages(messageIds) |
Promise<Map<string, Coordinate>> |
Neueste Koordinate je Nachricht. |
messageIdForShare(contactId, shareId) |
Promise<string \| null> |
Nachricht-Id zu einer Freigabe-Id. |
count() |
Promise<number> |
Anzahl der Koordinaten. |
SettingsRepository¶
| Name | Rückgabe | Beschreibung |
|---|---|---|
get(key) |
Promise<string \| null> |
Wert eines Schlüssels. |
set(key, value) |
Promise<void> |
Wert eines Schlüssels speichern. |
all() |
Promise<Record<string, string>> |
Alle Schlüssel und Werte. |
Die Memory-Repositories bieten zusätzlich clear() und insertMany(rows). Diese Hilfsmethoden braucht
MemoryDatabaseService für Import, Löschen und Demo-Daten.
Plattform-Weiche¶
src/app/app.config.ts stellt die Datenbank und alle Repositories als dataProviders bereit, die Leitung zur Antenne und den
Entsperr-Schlüssel als antennaProviders. useExisting bindet das abstrakte Token an eine konkrete, in root registrierte Instanz.
| Token | Browser | Android |
|---|---|---|
DatabaseService |
MemoryDatabaseService |
SqliteDatabaseService |
ContactRepository |
MemoryContactRepository |
SqliteContactRepository |
MessageRepository |
MemoryMessageRepository |
SqliteMessageRepository |
CoordinateRepository |
MemoryCoordinateRepository |
SqliteCoordinateRepository |
SettingsRepository |
MemorySettingsRepository |
SqliteSettingsRepository |
SerialTransport |
MockSerialTransport |
UsbSerialTransport (T06) |
KeystoreService |
BrowserKeystoreService |
AndroidKeystoreService |
Hilfsmodule (keine Services)¶
| Datei | Funktionen | Zweck |
|---|---|---|
src/app/core/crypto/hex.ts |
toHex(bytes), fromHex(hex) |
Bytes als Hex-Text in Kleinbuchstaben. fromHex wirft bei ungültigem Hex. |
src/app/core/crypto/base64.ts |
toBase64(bytes), fromBase64(base64) |
Bytes als Base64-Text. fromBase64 wirft bei ungültigem Base64. |
src/app/core/format/time.ts |
formatTime, formatListTime, formatLastSeen, formatDay |
Zeittexte in der Zeitzone Europe/Zurich. |
src/app/core/format/radio.ts |
formatMHz(freqHz) |
Frequenz in Hz als MHz-Text, z. B. 868100000 → „868“. |
src/app/core/messaging/limits.ts |
contentBytes, maxContentBytes, isValidDisplayName, splitText |
UTF-8-Bytelänge von Texten und Anzeigenamen, das gemeldete Textlimit und das Teilen langer Texte. |
src/app/core/messaging/reactions.ts |
REACTION_EMOJIS, isReactionEmoji |
Emoji-Tabelle der Reaktionen (Offgrid Protocol, Appendix B). |
src/app/core/antenna/device-parse.ts |
parseHello, parseContact, parseEvent, … |
Prüft Antworten und Ereignisse des Geräts und macht daraus Typen. |
src/app/core/keystore/unlock-wrap.ts, x25519.ts |
wrapToken, unwrapToken, x25519 |
Einpacken des Entsperr-Tokens (WebCrypto) und X25519 (RFC 7748). |
src/app/core/messaging/signal.ts |
signalLevel, signalLabel |
Signal-Level eines Kontakts und der Text dazu. |
src/app/core/location/geo.ts |
distanceMeters, bearingDegrees, compassName, formatDistance, formatCoordinate, formatCoordinatePlain, formatAccuracy, formatAgo, roundCoord |
Entfernung, Richtung und die Texte für die Koordinaten. |
Die Formate und die Signalstärke sind in Nachrichten beschrieben.
Wer nutzt was¶
| Aufrufer | Nutzt |
|---|---|
AppComponent |
AppStartService |
AppStartService |
DatabaseService, SettingsService, AntennaPort, MessagingService, HandshakeService und im Browser MemoryDatabaseService |
AntennaPort |
SerialTransport, KeystoreService, SerialClient |
UsbSerialTransport |
UsbSerial (natives Plugin) über das DI-Token USB_SERIAL_PLUGIN |
MessagingService |
AntennaPort, ContactRepository, MessageRepository, CoordinateRepository, HandshakeService, DataChangedService |
KontaktePage |
MessagingService, AntennaPort, SettingsService |
HandshakeService |
AntennaPort, ContactRepository, DataChangedService |
SettingsService |
SettingsRepository |
LocationService |
@capacitor/geolocation (nur nativ) |
| SQLite-Repositories | SqliteDatabaseService |
MemoryDatabaseService |
alle Memory-Repositories |