Skip to content

Commit de2043b

Browse files
committed
TCIServer simulator: redesign to use simple modules
1 parent 9d04e2d commit de2043b

File tree

1 file changed

+158
-101
lines changed

1 file changed

+158
-101
lines changed

devtools/TCISimulator/server.py

Lines changed: 158 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,173 @@
11
#!/usr/bin/env python3
2-
import time
3-
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
4-
5-
clients = []
6-
class SimpleTCI(WebSocket):
7-
8-
currFreq = 14000000
9-
currTrx = False
10-
currModulation = 'USB'
11-
currDrive = 20
12-
currRITOffset = 0
13-
currRITEnable = False
14-
currCWSpeed = 20
15-
currXITOffset = 0
16-
currXITEnable = False
17-
18-
def sendTCIMessage(self, msg):
19-
print(f"Sending msg: {msg}")
20-
time.sleep(0.1)
21-
for client in clients:
22-
client.sendMessage(msg + ";")
232

24-
def VFOCommand(self, args, getNum) :
25-
if ( len(args) > getNum ) :
3+
"""
4+
TCI WebSocket simulator
5+
6+
This script implements a minimal TCI-like WebSocket server that accepts multiple client connections
7+
and broadcasts status updates to all connected clients.
8+
9+
How it works
10+
- Starts a WebSocket server on TCP port 8000.
11+
- When a client connects, the server immediately sends a set of capability/status messages
12+
(limits, device info, supported modulations) and a READY message.
13+
- Incoming messages are parsed as simple "COMMAND:arg1,arg2,..." frames and mapped to handlers.
14+
Supported commands include: VFO, TRX, MODULATION, DRIVE, RIT_OFFSET, RIT_ENABLE, XIT_OFFSET,
15+
XIT_ENABLE, and CW_MACROS_SPEED.
16+
- The server keeps an internal state (frequency, TX/RX, modulation, drive, RIT/XIT settings, CW speed).
17+
When a command updates the state, the server broadcasts the corresponding response/status frame
18+
to all connected clients.
19+
20+
Notes
21+
- This is a simulator: it does not control real radio hardware; it only maintains and publishes state.
22+
- Each connection creates its own SimpleTCI state instance; broadcasting still goes to all clients.
23+
- Messages are terminated with ';' when sent to clients.
24+
"""
25+
26+
import asyncio
27+
import websockets
28+
29+
clients = set()
30+
31+
class SimpleTCI:
32+
def __init__(self):
33+
self.currFreq = 14000000
34+
self.currTrx = False
35+
self.currModulation = "USB"
36+
self.currDrive = 20
37+
self.currRITOffset = 0
38+
self.currRITEnable = False
39+
self.currCWSpeed = 20
40+
self.currXITOffset = 0
41+
self.currXITEnable = False
42+
43+
async def sendTCIMessage(self, msg: str):
44+
print(f"Sending msg: {msg}")
45+
await asyncio.sleep(0.1)
46+
dead = set()
47+
for ws in clients:
48+
try:
49+
await ws.send(msg + ";")
50+
except Exception:
51+
dead.add(ws)
52+
for ws in dead:
53+
clients.discard(ws)
54+
55+
async def VFOCommand(self, args, getNum):
56+
if len(args) > getNum:
2657
self.currFreq = args[2]
27-
self.sendTCIMessage(f"vfo:0,0,{self.currFreq};")
28-
29-
def TRXCommand(self, args, getNum) :
30-
if ( len(args) > getNum ) :
31-
self.currTrx = (args[1].lower() == 'true')
32-
self.sendTCIMessage(f"trx:0,{self.currTrx};")
33-
34-
def MODULATIONCommand(self, args, getNum) :
35-
if ( len(args) > getNum ) :
36-
self.currModulation = args[1].upper()
37-
self.sendTCIMessage(f"modulation:0,{self.currModulation}")
38-
39-
def DRIVECommand(self, args, getNum) :
40-
if ( len(args) > getNum ) :
58+
await self.sendTCIMessage(f"vfo:0,0,{self.currFreq}")
59+
60+
async def TRXCommand(self, args, getNum):
61+
if len(args) > getNum:
62+
self.currTrx = (args[1].lower() == "true")
63+
await self.sendTCIMessage(f"trx:0,{self.currTrx}")
64+
65+
async def MODULATIONCommand(self, args, getNum):
66+
if len(args) > getNum:
67+
self.currModulation = args[1].upper()
68+
await self.sendTCIMessage(f"modulation:0,{self.currModulation}")
69+
70+
async def DRIVECommand(self, args, getNum):
71+
if len(args) > getNum:
4172
self.currDrive = args[1]
42-
self.sendTCIMessage(f"drive:0,{self.currDrive}")
73+
await self.sendTCIMessage(f"drive:0,{self.currDrive}")
4374

44-
def RITOFFSETCommand(self, args, getNum) :
45-
if ( len(args) > getNum ) :
75+
async def RITOFFSETCommand(self, args, getNum):
76+
if len(args) > getNum:
4677
self.currRITOffset = args[1]
47-
self.sendTCIMessage(f"rit_offset:0,{self.currRITOffset}")
78+
await self.sendTCIMessage(f"rit_offset:0,{self.currRITOffset}")
4879

49-
def RITENABLECommand(self, args, getNum) :
50-
if ( len(args) > getNum ) :
51-
self.currRITEnable = (args[1].lower() == 'true')
52-
self.sendTCIMessage(f"rit_enable:0,{self.currRITEnable}")
80+
async def RITENABLECommand(self, args, getNum):
81+
if len(args) > getNum:
82+
self.currRITEnable = (args[1].lower() == "true")
83+
await self.sendTCIMessage(f"rit_enable:0,{self.currRITEnable}")
5384

54-
def XITOFFSETCommand(self, args, getNum) :
55-
if ( len(args) > getNum ) :
85+
async def XITOFFSETCommand(self, args, getNum):
86+
if len(args) > getNum:
5687
self.currXITOffset = args[1]
57-
self.sendTCIMessage(f"xit_offset:0,{self.currXITOffset}")
58-
59-
def XITENABLECommand(self, args, getNum) :
60-
if ( len(args) > getNum ) :
61-
self.currXITEnable = (args[1].lower() == 'true')
62-
self.sendTCIMessage(f"xit_enable:0,{self.currXITEnable}")
63-
64-
def CWMACROSSPEEDCommand(self, args, getNum) :
65-
if ( len(args) > getNum ) :
88+
await self.sendTCIMessage(f"xit_offset:0,{self.currXITOffset}")
89+
90+
async def XITENABLECommand(self, args, getNum):
91+
if len(args) > getNum:
92+
self.currXITEnable = (args[1].lower() == "true")
93+
await self.sendTCIMessage(f"xit_enable:0,{self.currXITEnable}")
94+
95+
async def CWMACROSSPEEDCommand(self, args, getNum):
96+
if len(args) > getNum:
6697
self.currCWSpeed = args[0]
67-
self.sendTCIMessage(f"cw_macros_speed:0,{self.currCWSpeed}")
98+
await self.sendTCIMessage(f"cw_macros_speed:0,{self.currCWSpeed}")
6899

69-
def processCommand(self, argString, getArgsNum, fct):
70-
stripArgs = argString.replace(';','')
100+
def _processCommand(self, argString, getArgsNum):
101+
stripArgs = argString.replace(";", "")
71102
args = stripArgs.split(",")
72-
if len(args) < getArgsNum :
73-
return
74-
fct(args, getArgsNum);
103+
if len(args) < getArgsNum:
104+
return None
105+
return args
75106

76-
def handleMessage(self):
77-
commandString = self.data.split(":");
107+
async def handleMessage(self, data: str):
108+
commandString = data.split(":")
78109
print(f"Received: {commandString}")
110+
111+
if len(commandString) < 2:
112+
return
113+
79114
command = commandString[0].upper()
80-
if command == "VFO" :
81-
self.processCommand(commandString[1], 2, self.VFOCommand)
82-
if command == "TRX" :
83-
self.processCommand(commandString[1], 1, self.TRXCommand)
84-
if command == "MODULATION" :
85-
self.processCommand(commandString[1], 1, self.MODULATIONCommand)
86-
if command == "DRIVE" :
87-
self.processCommand(commandString[1], 1, self.DRIVECommand)
88-
if command == "RIT_OFFSET" :
89-
self.processCommand(commandString[1], 1, self.RITOFFSETCommand)
90-
if command == "RIT_ENABLE" :
91-
self.processCommand(commandString[1], 1, self.RITENABLECommand)
92-
if command == "XIT_OFFSET" :
93-
self.processCommand(commandString[1], 1, self.XITOFFSETCommand)
94-
if command == "XIT_ENABLE" :
95-
self.processCommand(commandString[1], 1, self.XITENABLECommand)
96-
if command == "CW_MACROS_SPEED" :
97-
self.processCommand(commandString[1], 0, self.CWMACROSSPEEDCommand)
98-
99-
# for client in clients:
100-
# if client != self:
101-
# client.sendMessage(self.data);
102-
103-
def handleConnected(self):
104-
print(self.address, 'connected')
105-
clients.append(self)
106-
self.sendTCIMessage("VFO_LIMITS:10000,30000000;TRX_COUNT:2");
107-
self.sendTCIMessage("DEVICE:SunSDR2DX;MODULATIONS_LIST:AM,SAM,LSB,USB,CW,NFM,WFM;PROTOCOL:ExpertSDR3,1.9");
108-
self.sendTCIMessage("VFO:0,0,14000000;TRX:0,false;MODULATION:0,USB;RIT_OFFSET:0,-50;RIT_ENABLE:0,false");
109-
self.sendTCIMessage("READY;");
110-
111-
def handleClose(self):
112-
clients.remove(self)
113-
print(self.address, 'closed')
114-
115-
server = SimpleWebSocketServer('', 8000, SimpleTCI)
116-
server.serveforever()
115+
payload = commandString[1]
116+
117+
if command == "VFO":
118+
args = self._processCommand(payload, 2)
119+
if args: await self.VFOCommand(args, 2)
120+
elif command == "TRX":
121+
args = self._processCommand(payload, 1)
122+
if args: await self.TRXCommand(args, 1)
123+
elif command == "MODULATION":
124+
args = self._processCommand(payload, 1)
125+
if args: await self.MODULATIONCommand(args, 1)
126+
elif command == "DRIVE":
127+
args = self._processCommand(payload, 1)
128+
if args: await self.DRIVECommand(args, 1)
129+
elif command == "RIT_OFFSET":
130+
args = self._processCommand(payload, 1)
131+
if args: await self.RITOFFSETCommand(args, 1)
132+
elif command == "RIT_ENABLE":
133+
args = self._processCommand(payload, 1)
134+
if args: await self.RITENABLECommand(args, 1)
135+
elif command == "XIT_OFFSET":
136+
args = self._processCommand(payload, 1)
137+
if args: await self.XITOFFSETCommand(args, 1)
138+
elif command == "XIT_ENABLE":
139+
args = self._processCommand(payload, 1)
140+
if args: await self.XITENABLECommand(args, 1)
141+
elif command == "CW_MACROS_SPEED":
142+
args = self._processCommand(payload, 0)
143+
if args is not None: await self.CWMACROSSPEEDCommand(args, 0)
144+
145+
async def handler(websocket, path=None):
146+
clients.add(websocket)
147+
tci = SimpleTCI()
148+
149+
print(websocket.remote_address, "connected")
150+
151+
await tci.sendTCIMessage("VFO_LIMITS:10000,30000000;TRX_COUNT:2")
152+
await tci.sendTCIMessage("DEVICE:SunSDR2DX;MODULATIONS_LIST:AM,SAM,LSB,USB,CW,NFM,WFM;PROTOCOL:ExpertSDR3,1.9")
153+
await tci.sendTCIMessage("VFO:0,0,14000000;TRX:0,false;MODULATION:0,USB;RIT_OFFSET:0,-50;RIT_ENABLE:0,false")
154+
await tci.sendTCIMessage("READY")
155+
156+
try:
157+
async for message in websocket:
158+
await tci.handleMessage(message)
159+
finally:
160+
clients.discard(websocket)
161+
print(websocket.remote_address, "closed")
162+
163+
async def main():
164+
host = "0.0.0.0" # nebo "127.0.0.1" jen pro localhost
165+
port = 8000
166+
167+
print(f"TCI Server Simulator listening on ws://{host}:{port}")
168+
169+
async with websockets.serve(handler, host, port):
170+
await asyncio.Future() # run forever
171+
172+
if __name__ == "__main__":
173+
asyncio.run(main())

0 commit comments

Comments
 (0)