Files
dwarf-go/dwarfctl/proto/merge.py
Jacquin Antoine 814a836c5a Reverse-engineer DWARF II telescope API and build open-source Go client
Complete reverse-engineering of the DWARFLAB Android app (v3.4.0) protocol
and implementation of a working CLI tool to control DWARF II telescopes.

Analysis (from APK decompilation with jadx):
- Extracted 17 protobuf definitions (382 messages) from embedded descriptors
- Mapped all 323 WebSocket command IDs across 16 modules
- Documented the full protocol: BLE discovery, WebSocket control (port 9900),
  RTSP preview, WsPacket envelope (proto v2.3)
- Documented the Android UI structure (screens, navigation, shooting modes)
- Key discovery: telescope responds with type=3 (reply), not type=1 (response),
  and several commands are fire-and-forget (RGB, camera open/close)

dwarfctl Go client:
- Protobuf bindings generated from extracted .proto files (397 messages)
- WebSocket transport layer with request-response matching and notification fan-out
- Typed API covering cameras, motors, astrophotography, focus, tracking, system, power
- Cobra CLI with 30+ subcommands and --debug traffic logging
- 57 unit tests (transport round-trip, command routing, proto encoding)
- Validated on real hardware: state, photo, motor slew (all directions/speeds),
  focus, RGB, time/location sync all confirmed working

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-12 15:18:56 +02:00

64 lines
2.1 KiB
Python

#!/usr/bin/env python3
"""Merge all extracted .proto files into a single unified proto3 file,
resolving duplicate names by prefixing with the source module."""
import os, re, sys
PROTO_DIR = sys.argv[1]
OUT = sys.argv[2]
# Collect all message/enum blocks
all_blocks = [] # (kind, name, lines, source_file)
seen_names = {}
duplicates = set()
for fn in sorted(os.listdir(PROTO_DIR)):
if not fn.endswith('.proto'):
continue
module = fn.replace('.proto','')
txt = open(os.path.join(PROTO_DIR, fn)).read()
lines = txt.split('\n')
i = 0
while i < len(lines):
line = lines[i]
# Skip syntax/package/import/option lines
if re.match(r'^(syntax|package|import|option)\s', line):
i += 1
continue
# Find message or enum blocks
m = re.match(r'^(message|enum)\s+(\w+)\s*\{', line)
if m:
kind = m.group(1)
name = m.group(2)
depth = 1
block_lines = [line]
i += 1
while i < len(lines) and depth > 0:
depth += lines[i].count('{') - lines[i].count('}')
block_lines.append(lines[i])
i += 1
# Handle duplicate names
final_name = name
if name in seen_names:
final_name = module.capitalize() + name
duplicates.add(name)
seen_names[final_name] = True
all_blocks.append((kind, final_name, block_lines, module))
else:
i += 1
# Write unified proto
with open(OUT, 'w') as f:
f.write('syntax = "proto3";\n')
f.write('option go_package = "github.com/antitbone/dwarfctl/proto;pb";\n\n')
for kind, name, block_lines, module in all_blocks:
f.write(f'// source: {module}\n')
# Replace the first line's name if it was renamed
block_lines[0] = re.sub(
rf'^({kind})\s+\w+\s*\{{',
rf'\1 {name} {{',
block_lines[0])
f.write('\n'.join(block_lines) + '\n\n')
print(f'Merged {len(all_blocks)} blocks into {OUT}')
print(f'Renamed duplicates: {duplicates}')