Join one FM-Funknetz talk group from a headset window.

The station reads ~/.funknetz.json, speaks protocol 2.0, and builds an unsigned funknetz.app.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Konrad Neitzel
2026-10-03 16:21:41 +02:00
co-authored by Cursor
parent 8e75515460
commit 9c4f6621fc
23 changed files with 2933 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
package station
import (
"encoding/binary"
"fmt"
"io"
)
const (
tcpHeartbeat = 1
tcpProtoVer = 5
tcpAuthChallenge = 10
tcpAuthResponse = 11
tcpAuthOK = 12
tcpError = 13
tcpServerInfo = 100
tcpTalkerStart = 104
tcpTalkerStop = 105
tcpSelectTG = 106
udpHeartbeat = 1
udpAudio = 101
udpFlush = 102
maxFrame = 32768
)
type frame struct {
typ uint16
body []byte
}
func writeFrame(w io.Writer, typ uint16, body []byte) error {
payload := make([]byte, 2+len(body))
binary.BigEndian.PutUint16(payload[:2], typ)
copy(payload[2:], body)
var hdr [4]byte
binary.BigEndian.PutUint32(hdr[:], uint32(len(payload)))
if _, err := w.Write(hdr[:]); err != nil {
return err
}
_, err := w.Write(payload)
return err
}
func readFrame(r io.Reader) (frame, error) {
var hdr [4]byte
if _, err := io.ReadFull(r, hdr[:]); err != nil {
return frame{}, err
}
n := binary.BigEndian.Uint32(hdr[:])
if n < 2 || n > maxFrame {
return frame{}, fmt.Errorf("frame length %d", n)
}
buf := make([]byte, n)
if _, err := io.ReadFull(r, buf); err != nil {
return frame{}, err
}
return frame{typ: binary.BigEndian.Uint16(buf[:2]), body: buf[2:]}, nil
}
func appendU16(b []byte, v uint16) []byte {
var buf [2]byte
binary.BigEndian.PutUint16(buf[:], v)
return append(b, buf[:]...)
}
func appendString(b []byte, s string) []byte {
b = appendU16(b, uint16(len(s)))
return append(b, s...)
}
func appendBlob(b []byte, p []byte) []byte {
b = appendU16(b, uint16(len(p)))
return append(b, p...)
}
func parseU16(b []byte) (uint16, []byte, error) {
if len(b) < 2 {
return 0, nil, io.ErrUnexpectedEOF
}
return binary.BigEndian.Uint16(b[:2]), b[2:], nil
}
func parseU32(b []byte) (uint32, []byte, error) {
if len(b) < 4 {
return 0, nil, io.ErrUnexpectedEOF
}
return binary.BigEndian.Uint32(b[:4]), b[4:], nil
}
func parseString(b []byte) (string, []byte, error) {
n, rest, err := parseU16(b)
if err != nil {
return "", nil, err
}
if len(rest) < int(n) {
return "", nil, io.ErrUnexpectedEOF
}
return string(rest[:n]), rest[n:], nil
}
func parseBlob(b []byte) ([]byte, []byte, error) {
n, rest, err := parseU16(b)
if err != nil {
return nil, nil, err
}
if len(rest) < int(n) {
return nil, nil, io.ErrUnexpectedEOF
}
return rest[:n], rest[n:], nil
}
func parseStringList(b []byte) ([]string, []byte, error) {
n, rest, err := parseU16(b)
if err != nil {
return nil, nil, err
}
out := make([]string, 0, n)
for range int(n) {
s, next, err := parseString(rest)
if err != nil {
return nil, nil, err
}
out = append(out, s)
rest = next
}
return out, rest, nil
}
func parseServerInfo(b []byte) (uint16, []string, error) {
_, rest, err := parseU16(b)
if err != nil {
return 0, nil, err
}
id, rest, err := parseU16(rest)
if err != nil {
return 0, nil, err
}
_, rest, err = parseStringList(rest)
if err != nil {
return 0, nil, err
}
codecs, _, err := parseStringList(rest)
if err != nil {
return 0, nil, err
}
return id, codecs, nil
}
func hasOPUS(codecs []string) bool {
for _, c := range codecs {
if c == "OPUS" {
return true
}
}
return false
}