diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e0cd3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.py[co] +*~ +stations.conf +recordings/* +sounds/* diff --git a/frn/protocol/client.py b/frn/protocol/client.py index 5438f2c..68bf730 100644 --- a/frn/protocol/client.py +++ b/frn/protocol/client.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +from Queue import Queue from twisted.internet.protocol import ClientFactory from twisted.protocols.basic import LineReceiver +from twisted.internet.task import LoopingCall +from twisted.python import log from frn.utility import * @@ -10,6 +13,7 @@ class FRNClient(LineReceiver): client_version = 2010002 def connectionMade(self): + self.txq = Queue() self.login() def ready(self): @@ -34,7 +38,7 @@ class FRNClient(LineReceiver): self.msgbuffer.append(line) self.phase += 1 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 self.ready() handler(message) @@ -54,7 +58,7 @@ class FRNClient(LineReceiver): audio_data = self.msgbuffer source = ord(audio_data[0])*256+ord(audio_data[1]) self.ready() - self.on_AUDIO(source, audio_data[2:]) + self.decodeAUDIO(source, audio_data[2:]) if len(data) > needed: self.dataReceived(data[needed:]) @@ -65,7 +69,7 @@ class FRNClient(LineReceiver): self.phase = 1 else: self.serverdata = parseSimpleXML(line.strip()) - print self.serverdata + self.loginResponse(self.serverdata) if int(self.serverdata['sv']) > 2009004: self.sendLine(makeAuthKey(self.serverdata['kp'])) self.ready() @@ -76,9 +80,9 @@ class FRNClient(LineReceiver): if self.status == 'READY': packet_type = ord(data[0]) if packet_type == 0: # Keepalive - self.sendLine('P') + self.pong() elif packet_type == 1: # TX ack - self.status == 'TX' + self.status = 'TX' self.phase = 0 if len(data) > 1: self.dataReceived(data[1:]) @@ -86,18 +90,21 @@ class FRNClient(LineReceiver): self.startAudioMessage(data[1:]) elif packet_type == 3: # Client list self.startMultiLineMessage('CLIENTS', data[1:]) - elif packet_type == 4: # SMS - self.startMultiLineMessage('SMS', data[1:]) + elif packet_type == 4: # Text + self.startMultiLineMessage('TEXT', data[1:]) elif packet_type == 5: # Channel list - self.startMultiLineMessage('CHANNELS', data[1:]) + self.startMultiLineMessage('NETWORKS', data[1:]) else: - print "Unknown packet type %d" % packet_type + log.err("Unknown packet type %d" % packet_type) elif self.status == 'AUDIO': self.collectAudioMessage(data) elif self.status == 'TX': if self.phase == 0: 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): d = self.factory.client_id @@ -116,31 +123,82 @@ class FRNClient(LineReceiver): self.status = 'AUTH' self.phase = 0 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)) - def request_rx(self): + def stopTransmission(self): self.sendLine('RX0') - def request_tx(self): + def startTransmission(self): self.sendLine('TX0') - def send_audio(self, frame): + def sendAudioFrame(self, frame): self.sendLine('TX1') 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))) - def on_unimplemented(self, msg): - print msg + def unimplemented(self, 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 - 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 @@ -152,16 +210,16 @@ class FRNClientFactory(ClientFactory): self.client_id = kw def startedConnecting(self, connector): - print 'Started to connect.' + log.msg('Started to connect') def buildProtocol(self, addr): - print 'Connected.' + log.msg('Connected') return ClientFactory.buildProtocol(self, addr) def clientConnectionLost(self, connector, reason): - print 'Lost connection. Reason:', reason + log.msg('Lost connection. Reason: %s' % 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: diff --git a/parrot.py b/parrot.py old mode 100644 new mode 100755 index 98e09c3..6a3c57e --- a/parrot.py +++ b/parrot.py @@ -3,29 +3,98 @@ from __future__ import with_statement 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): - def on_SMS(self, msg): - print "Messaggio %s da %s: %s" % (msg[2], msg[0], msg[1]) - if msg[2][0] == 'P': # Risponde solo ai messaggi privati - self.send_SMS(msg[0], msg[1]) + def getClientName(self, client_id): + if self.clientsById.has_key(client_id): + return self.clientsById[client_id]['on'] + else: + return client_id - def on_AUDIO(self, from_id, frames): - print "AUDIO from %d (%d bytes)" % (from_id, len(frames)) - with file('rx-%d.gsm' % from_id, 'ab') as f: + def textMessageReceived(self, client, message, target): + log.msg("Type %s message from %s: %s" % + (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) + 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): protocol = FRNParrot + reactor = reactor if __name__ == '__main__': import sys from ConfigParser import ConfigParser - from twisted.internet import reactor + + log.startLogging(sys.stderr) cfg = ConfigParser() cfg.read('stations.conf') @@ -36,7 +105,7 @@ if __name__ == '__main__': stations = cfg.sections() stations.sort() 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))