Jump to content

SimTX API/Quick start

From SimTX Wiki
Revision as of 17:14, 24 October 2026 by Clanker (talk | contribs) (Remove development history and server internals)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This page gets a program receiving from SimTX with nothing but a standard Python 3 installation. It connects anonymously, places an ad-hoc receiver in Budapest on 7.1 MHz, and prints the power of the first ten IQ blocks. It uses only the standard library, including a small hand-written protobuf codec.

For real software, generate bindings from the .proto files with protoc instead of hand-encoding messages.

The program[edit | edit source]

import math
import socket
import ssl
import struct

HOST, PORT = "argon.simtx.net", 5556


# Protobuf encoding: just enough for the two messages this client sends.

def varint(n):
    out = bytearray()
    while True:
        low, n = n & 0x7F, n >> 7
        out.append(low | 0x80 if n else low)
        if not n:
            return bytes(out)


def f_varint(field, value):          # int32, int64, bool and enum fields
    return varint(field << 3 | 0) + varint(value)


def f_double(field, value):
    return varint(field << 3 | 1) + struct.pack("<d", value)


def f_float(field, value):
    return varint(field << 3 | 5) + struct.pack("<f", value)


def f_bytes(field, data):            # string, bytes and nested message fields
    if isinstance(data, str):
        data = data.encode("utf-8")
    return varint(field << 3 | 2) + varint(len(data)) + data


# Protobuf decoding: a message becomes {field number: last value seen}.

def read_varint(buf, i):
    value = shift = 0
    while True:
        b = buf[i]
        i += 1
        value |= (b & 0x7F) << shift
        shift += 7
        if not b & 0x80:
            return value, i


def decode(buf):
    fields, i = {}, 0
    while i < len(buf):
        key, i = read_varint(buf, i)
        field, wire_type = key >> 3, key & 7
        if wire_type == 0:
            value, i = read_varint(buf, i)
        elif wire_type == 1:
            value, i = buf[i:i + 8], i + 8
        elif wire_type == 2:
            n, i = read_varint(buf, i)
            value, i = buf[i:i + n], i + n
        elif wire_type == 5:
            value, i = buf[i:i + 4], i + 4
        else:
            raise ValueError(f"unexpected wire type {wire_type}")
        fields[field] = value
    return fields


def f64(fields, n):
    return struct.unpack("<d", fields[n])[0] if n in fields else 0.0


def f32(fields, n):
    return struct.unpack("<f", fields[n])[0] if n in fields else 0.0


def text(fields, n):
    return fields.get(n, b"").decode("utf-8", "replace")


# Framing: varint length, then one ClientFrame or ServerFrame.

def read_exact(sock, n):
    buf = bytearray()
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise ConnectionError("server closed the connection")
        buf += chunk
    return bytes(buf)


def read_frame(sock):
    length = shift = 0
    while True:
        b = read_exact(sock, 1)[0]
        length |= (b & 0x7F) << shift
        shift += 7
        if not b & 0x80:
            break
    frame = decode(read_exact(sock, length))
    for member, payload in frame.items():   # a frame holds one oneof member
        return member, payload
    return 0, b""                            # empty frame: skip it


def send_frame(sock, member, message):
    frame = f_bytes(member, message)
    sock.sendall(varint(len(frame)) + frame)


raw = socket.create_connection((HOST, PORT), timeout=10)
raw.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock = ssl.create_default_context().wrap_socket(raw, server_hostname=HOST)

# Preface: "SIMT", protocol version 25
sock.sendall(struct.pack(">II", 0x53494D54, 25))

# hello (1): ClientHello { role: ROLE_RECEIVER, client_name, client_version }
send_frame(sock, 1, f_varint(1, 1) + f_bytes(4, "quick-start.py") + f_bytes(5, "1.0"))

member, payload = read_frame(sock)
hello = decode(payload)
if member != 1 or hello.get(1, 0) != 1:      # hello_result, HELLO_STATUS_ACCEPTED
    raise SystemExit(f"hello rejected ({hello.get(1, 0)}): {text(hello, 2)}")

# receiver_start (10): an ad-hoc receiver in Budapest with a 10 m dipole, on 7.1 MHz
location = f_double(1, 47.4979) + f_double(2, 19.0402) + f_double(3, 120.0)
antenna = f_bytes(1, "DIPOLE") + f_float(2, 10.0)
adhoc = f_bytes(1, "N0CALL") + f_bytes(2, location) + f_bytes(3, antenna)
start = (
    f_bytes(1, f_bytes(2, adhoc))            # site { adhoc { ... } }
    + f_double(3, 7_100_000.0)               # center_frequency_hz
    + f_float(4, 48_000.0)                   # bandwidth_hz
    + f_varint(5, 48_000)                    # sample_rate
    + f_varint(6, 4_800)                     # chunk_samples
)
send_frame(sock, 10, start)

chunk, int8, blocks = 0, False, 0
while blocks < 10:
    member, payload = read_frame(sock)
    message = decode(payload)
    if member == 10:                         # session_status
        state = message.get(1, 0)
        if state != 1:                       # anything but SESSION_STATE_BOUND
            raise SystemExit(f"session state {state}, code {message.get(3, 0)}: {text(message, 2)}")
        print("bound, session id", message.get(4, b"").hex())
    elif member == 11:                       # iq_descriptor
        rate, chunk, int8 = message.get(1, 0), message.get(3, 0), message.get(4, 0) == 1
        print(f"stream: {rate} S/s at {f64(message, 2):.0f} Hz, {chunk} samples/block")
    elif member == 12:                       # iq
        scale, samples = f32(message, 1), message.get(2, b"")
        count = len(samples) if int8 else len(samples) // 2
        values = struct.unpack((">%db" if int8 else ">%dh") % count, samples)
        full_scale = 127 if int8 else 32767
        power = sum((v * scale / full_scale) ** 2 for v in values) / chunk
        sequence = struct.unpack("<Q", message[5])[0] if 5 in message else 0
        print(f"block {sequence}: {-12 + 10 * math.log10(max(power, 1e-30)):.1f} dBm")
        blocks += 1
    elif member == 3:                        # error
        raise SystemExit(f"error {message.get(1, 0)}: {text(message, 2)}")
    # pong, banner and frames this client does not know: ignore

sock.close()

What it does[edit | edit source]

  1. Connects with TLS to the public server and sets TCP_NODELAY. Real software should take the host and port from the server directory or from its configuration; they are hard-coded here for brevity.
  2. Sends the preface, the 8 bytes 53 49 4D 54 00 00 00 19: "SIMT" and the protocol version 25. See Preface.
  3. Sends a ClientHello as ClientFrame member 1, with role = ROLE_RECEIVER (1). auth is left at 0, AUTH_KIND_ANONYMOUS, so no fields are needed for it: protobuf omits zero values.
  4. Reads the ServerHello (ServerFrame member 1) and stops unless its status is HELLO_STATUS_ACCEPTED (1).
  5. Sends a ReceiverStart (member 10) whose site is an AdhocStation: callsign, position and a dipole 10 m above ground. A wire_length_meters of 0 sizes the dipole for the frequency.
  6. Reads frames in a loop and dispatches on the member number: SessionStatus (10) must say SESSION_STATE_BOUND; IqStreamDescriptor (11) gives the negotiated rate and block size; each IqBlock (12) is decoded and its power printed; an Error (3) stops the program. Every other frame is ignored, as the protocol requires.

Things to notice[edit | edit source]

  • Two byte orders. Protobuf scalars such as double, float and fixed64 are little-endian. The IQ sample bytes inside IqBlock.samples are big-endian int16. See Conventions.
  • The server decides. The program asks for 4,800 samples per block. argon.simtx.net caps blocks at 40 ms, so at 48,000 samples per second it delivers 1,920. Always use the values in the IqStreamDescriptor. See Negotiation.
  • Power. A sample magnitude of 1.0 is −12 dBm at the receiver input, so a block's mean power in dBm is −12 + 10 × log10(mean(I² + Q²)).
  • Sequence numbers. Blocks are numbered from 1. A gap in sequence means the client fell behind and the server dropped blocks.
  • Rejections are explicit. If the server cannot place the receiver, for example because a limit is reached, the SessionStatus says SESSION_STATE_REJECTED with an ErrorCode and a reason, and the server closes the connection.

Going further[edit | edit source]

  • Retune at any time with a ReceiverRetune (member 11): send_frame(sock, 11, f_double(1, 7_074_000.0) + f_varint(2, 1)). The second field is a request_id; the server confirms with a SessionStatus UPDATED carrying it.
  • Timestamps: add f_varint(8, 1) to the ReceiverStart to set timestamped. Every IqBlock then carries the UTC instant of its first sample in fields 3 and 4.
  • Receive through a station instead of an ad-hoc site: sign in, mint a receiver game ticket for the station, send it in the hello with auth = AUTH_KIND_GAME_TICKET, and set site.station to a StationBinding. See SimTX API/Stations.
  • Transmit: transmitting needs an account with game.transmit and one of your stations. SimTX API/Transmitter session walks through it.

Next steps[edit | edit source]

SimTX API reference (protocol v25)
Start OverviewQuick startFramingData typesPacket listSchemaLimits and errorsCompatibility and versioning
Account service OverviewDevice loginServer directoryStation endpointsGame ticketsPermissionsPublic endpoints
Game protocol Connection and handshakeSession statusReceiver sessionTransmitter sessionControl sessionCatalogModeration
Stations StationsSatellitesTracking and pointing
Connection ClientHelloPingServerHelloPongErrorBanner
Session AntennaPointingUpdateSessionStatus
Receiver ForwardedClientAddressReceiverStartReceiverRetuneIqStreamDescriptorIqBlock
Transmitter TransmitterStartSampleBlockTxUpdateTxModeInfoTxQueueTxAcceptTxTelemetryTxUpdateAck
Control ListStationsStartMonitorStopMonitorWeatherMapRequestServerHealthRequestCoverageMapRequestEphemerisRequestPassPredictionRequestStationListMonitorBlockMonitorEndWeatherMapResponseServerHealthReportCoverageMapResponseEphemerisResponsePassPredictionResponseMonitorAck
Catalog CatalogListRequestCatalogFetchCatalogUploadCatalogDeleteCatalogListCatalogEntryCatalogUploadResultCatalogDeleteResult
Moderation KickRequestIpModerationRequestBroadcastBannerKickResultIpModerationResultStationImpactList
Archive Protocol v24 reference