Cleaned up FRNClient protocol API

Parrot fully working
This commit is contained in:
Maurizio Porrato 2010-08-09 16:40:19 +02:00
parent efe8685371
commit abd4301390
3 changed files with 165 additions and 33 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*.py[co]
*~
stations.conf
recordings/*
sounds/*

View File

@ -1,7 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from Queue import Queue
from twisted.internet.protocol import ClientFactory from twisted.internet.protocol import ClientFactory
from twisted.protocols.basic import LineReceiver from twisted.protocols.basic import LineReceiver
from twisted.internet.task import LoopingCall
from twisted.python import log
from frn.utility import * from frn.utility import *
@ -10,6 +13,7 @@ class FRNClient(LineReceiver):
client_version = 2010002 client_version = 2010002
def connectionMade(self): def connectionMade(self):
self.txq = Queue()
self.login() self.login()
def ready(self): def ready(self):
@ -34,7 +38,7 @@ class FRNClient(LineReceiver):
self.msgbuffer.append(line) self.msgbuffer.append(line)
self.phase += 1 self.phase += 1
if self.phase >= self.expected_lines: if self.phase >= self.expected_lines:
handler = getattr(self, 'on_'+self.status, self.on_unimplemented) handler = getattr(self, 'decode'+self.status, self.unimplemented)
message = self.msgbuffer message = self.msgbuffer
self.ready() self.ready()
handler(message) handler(message)
@ -54,7 +58,7 @@ class FRNClient(LineReceiver):
audio_data = self.msgbuffer audio_data = self.msgbuffer
source = ord(audio_data[0])*256+ord(audio_data[1]) source = ord(audio_data[0])*256+ord(audio_data[1])
self.ready() self.ready()
self.on_AUDIO(source, audio_data[2:]) self.decodeAUDIO(source, audio_data[2:])
if len(data) > needed: if len(data) > needed:
self.dataReceived(data[needed:]) self.dataReceived(data[needed:])
@ -65,7 +69,7 @@ class FRNClient(LineReceiver):
self.phase = 1 self.phase = 1
else: else:
self.serverdata = parseSimpleXML(line.strip()) self.serverdata = parseSimpleXML(line.strip())
print self.serverdata self.loginResponse(self.serverdata)
if int(self.serverdata['sv']) > 2009004: if int(self.serverdata['sv']) > 2009004:
self.sendLine(makeAuthKey(self.serverdata['kp'])) self.sendLine(makeAuthKey(self.serverdata['kp']))
self.ready() self.ready()
@ -76,9 +80,9 @@ class FRNClient(LineReceiver):
if self.status == 'READY': if self.status == 'READY':
packet_type = ord(data[0]) packet_type = ord(data[0])
if packet_type == 0: # Keepalive if packet_type == 0: # Keepalive
self.sendLine('P') self.pong()
elif packet_type == 1: # TX ack elif packet_type == 1: # TX ack
self.status == 'TX' self.status = 'TX'
self.phase = 0 self.phase = 0
if len(data) > 1: if len(data) > 1:
self.dataReceived(data[1:]) self.dataReceived(data[1:])
@ -86,18 +90,21 @@ class FRNClient(LineReceiver):
self.startAudioMessage(data[1:]) self.startAudioMessage(data[1:])
elif packet_type == 3: # Client list elif packet_type == 3: # Client list
self.startMultiLineMessage('CLIENTS', data[1:]) self.startMultiLineMessage('CLIENTS', data[1:])
elif packet_type == 4: # SMS elif packet_type == 4: # Text
self.startMultiLineMessage('SMS', data[1:]) self.startMultiLineMessage('TEXT', data[1:])
elif packet_type == 5: # Channel list elif packet_type == 5: # Channel list
self.startMultiLineMessage('CHANNELS', data[1:]) self.startMultiLineMessage('NETWORKS', data[1:])
else: else:
print "Unknown packet type %d" % packet_type log.err("Unknown packet type %d" % packet_type)
elif self.status == 'AUDIO': elif self.status == 'AUDIO':
self.collectAudioMessage(data) self.collectAudioMessage(data)
elif self.status == 'TX': elif self.status == 'TX':
if self.phase == 0: if self.phase == 0:
self.phase = 1 self.phase = 1
self.on_TX(ord(data[0])*256+ord(data[1])) self.decodeTX(ord(data[0])*256+ord(data[1]))
self.ready()
if len(data) > 2:
self.dataReceived(data[2:])
def login(self): def login(self):
d = self.factory.client_id d = self.factory.client_id
@ -116,31 +123,82 @@ class FRNClient(LineReceiver):
self.status = 'AUTH' self.status = 'AUTH'
self.phase = 0 self.phase = 0
self.sendLine(ap) self.sendLine(ap)
#self.request_rx()
def set_status(self, status): def pong(self):
self.sendLine('P')
def setStatus(self, status):
self.sendLine('ST:%s' % str(status)) self.sendLine('ST:%s' % str(status))
def request_rx(self): def stopTransmission(self):
self.sendLine('RX0') self.sendLine('RX0')
def request_tx(self): def startTransmission(self):
self.sendLine('TX0') self.sendLine('TX0')
def send_audio(self, frame): def sendAudioFrame(self, frame):
self.sendLine('TX1') self.sendLine('TX1')
self.transport.write(frame) self.transport.write(frame)
def send_SMS(self, dest, text): def streamStep(self, count):
if count > 1:
log.msg("WARNING: lost %d ticks" % (count-1))
for i in range(count):
self.sendAudioFrame(self.txq.get_nowait())
def stopStreaming(self):
self.txtimer.stop()
def _streamAck(self):
self.txtimer = LoopingCall.withCount(self.streamStep)
self.txtimer.start(0.20).addCallback(
lambda _: self.stopTransmission()).addErrback(
lambda _: self.stopTransmission())
def feedStreaming(self, frames):
if type(frames) == list:
for frame in frames:
self.txq.put_nowait(frame)
else:
self.txq.put_nowait(frames)
def startStreaming(self):
self.startTransmission()
def sendTextMessage(self, dest, text):
self.sendLine('TM:'+formatSimpleXML(dict(ID=dest, MS=text))) self.sendLine('TM:'+formatSimpleXML(dict(ID=dest, MS=text)))
def on_unimplemented(self, msg): def unimplemented(self, msg):
print msg log.msg("Unimplemented: %s" % msg)
def on_AUDIO(self, from_id, frames): def decodeAUDIO(self, from_id, frames):
self.audioFrameReceived(from_id, frames)
def decodeTX(self, my_id):
self._streamAck()
def decodeTEXT(self, msg):
self.textMessageReceived(msg[0], msg[1], msg[2])
def decodeCLIENTS(self, msg):
self.clientsListUpdated([parseSimpleXML(x) for x in msg])
def decodeNETWORKS(self, msg):
self.networksListUpdated(msg)
def loginResponse(self, info):
pass pass
def on_TX(self, my_id): def audioFrameReceived(self, from_id, frame):
pass
def textMessageReceived(self, client, message, target):
pass
def clientsListUpdated(self, clients):
pass
def networksListUpdated(self, networks):
pass pass
@ -152,16 +210,16 @@ class FRNClientFactory(ClientFactory):
self.client_id = kw self.client_id = kw
def startedConnecting(self, connector): def startedConnecting(self, connector):
print 'Started to connect.' log.msg('Started to connect')
def buildProtocol(self, addr): def buildProtocol(self, addr):
print 'Connected.' log.msg('Connected')
return ClientFactory.buildProtocol(self, addr) return ClientFactory.buildProtocol(self, addr)
def clientConnectionLost(self, connector, reason): def clientConnectionLost(self, connector, reason):
print 'Lost connection. Reason:', reason log.msg('Lost connection. Reason: %s' % reason)
def clientConnectionFailed(self, connector, reason): def clientConnectionFailed(self, connector, reason):
print 'Connection failed. Reason:', reason log.err('Connection failed. Reason: %s' % reason)
# vim: set et ai sw=4 ts=4 sts=4: # vim: set et ai sw=4 ts=4 sts=4:

87
parrot.py Normal file → Executable file
View File

@ -3,29 +3,98 @@
from __future__ import with_statement from __future__ import with_statement
from frn.protocol.client import FRNClient, FRNClientFactory from frn.protocol.client import FRNClient, FRNClientFactory
from twisted.internet import reactor, task
from twisted.internet.defer import DeferredList
from twisted.python import log
import os, string
safe_chars = string.ascii_letters+string.digits+' :;.,+-=$@'
PARROT_AUDIO_DELAY = 5.0
def sanitizeFilename(name):
r = ''
for c in name:
if c in safe_chars:
r += c
return r
class FRNParrot(FRNClient): class FRNParrot(FRNClient):
def on_SMS(self, msg): def getClientName(self, client_id):
print "Messaggio %s da %s: %s" % (msg[2], msg[0], msg[1]) if self.clientsById.has_key(client_id):
if msg[2][0] == 'P': # Risponde solo ai messaggi privati return self.clientsById[client_id]['on']
self.send_SMS(msg[0], msg[1]) else:
return client_id
def on_AUDIO(self, from_id, frames): def textMessageReceived(self, client, message, target):
print "AUDIO from %d (%d bytes)" % (from_id, len(frames)) log.msg("Type %s message from %s: %s" %
with file('rx-%d.gsm' % from_id, 'ab') as f: (target, self.getClientName(client), message))
if target == 'P': # Only reply to private messages
if not message.startswith('play'):
self.sendTextMessage(client, message)
else:
cmd = message.split()
if len(cmd) > 1:
message = sanitizeFilename(cmd[1])
else:
message = 'monkeys'
filename = 'sounds/%s.wav' % message
if os.path.exists(filename):
log.msg("Streaming file %s" % filename)
with file(filename, 'rb') as sf:
sf.seek(0x3c) # Skip wav header
while True:
b = sf.read(325)
if len(b) < 325:
break
self.feedStreaming(b)
self.factory.reactor.callLater(0.5,
self.startStreaming)
else:
self.sendTextMessage(client, "File not found")
def stopTransmission(self):
FRNClient.stopTransmission(self)
log.msg("Stopped playback.")
def startRepeating(self, from_id):
log.msg("%s stopped talking: starting playback." %
self.clients[from_id-1]['on'])
self.startStreaming()
def audioFrameReceived(self, from_id, frames):
recname = sanitizeFilename(self.clients[from_id-1]['on'])
with file('recordings/%s.gsm' % recname, 'ab') as f:
f.write(frames) f.write(frames)
self.feedStreaming(frames)
try:
self.parrot_timer.reset(PARROT_AUDIO_DELAY)
except:
log.msg("%s started talking" %
self.clients[from_id-1]['on'])
self.parrot_timer = self.factory.reactor.callLater(
PARROT_AUDIO_DELAY, self.startRepeating, from_id)
self.pong()
def loginResponse(self, info):
log.msg("Login: %s" % info['al'])
def clientsListUpdated(self, clients):
self.clients = clients
self.clientsById = dict([(i['id'], i) for i in clients])
class FRNParrotFactory(FRNClientFactory): class FRNParrotFactory(FRNClientFactory):
protocol = FRNParrot protocol = FRNParrot
reactor = reactor
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
from ConfigParser import ConfigParser from ConfigParser import ConfigParser
from twisted.internet import reactor
log.startLogging(sys.stderr)
cfg = ConfigParser() cfg = ConfigParser()
cfg.read('stations.conf') cfg.read('stations.conf')
@ -36,7 +105,7 @@ if __name__ == '__main__':
stations = cfg.sections() stations = cfg.sections()
stations.sort() stations.sort()
station = cfg.sections()[0] station = cfg.sections()[0]
print "Profile not specified: using '%s'" % station log.msg("Profile not specified: using '%s'" % station)
station_conf = dict(cfg.items(station)) station_conf = dict(cfg.items(station))