Legacy actors#
The opscore protocol#
The tron and opscore products define a protocol that has been implemented under different frameworks (e.g., actorcore, twistedActor). Very simplistically, the actor structure can be visualised as
As we saw in the previous section, actors are both producers of commands and replies, and consumers of them. In the case of legacy/opscore actors, all communication is routed through a central message broker called Tron
.
Tron (often called the hub) acts as a TCP server (usually on port 6093) to which the actors and clients/commanders can connect to. Similarly, each actor (but not clients) is a TCP server. Tron receives commands through its own socket, parses the header of the command, and then sends it to the appropriate actor through the actor port. The actor executes the command and replies via its port. Tron gets the reply and broadcasts it to all the listening actors.
Commands#
Commands are sent to Tron as a string over the TCP socket. The format of the command header changes depending on how the Tron decoder has been configured, but for SDSS the format is as follows
program.client message_id target command_string
where program.client
are the program and name of the commander, message_id
is a positive integer assigned by the commander to identify this command and its replies, target
is the actor being commanded, and command_string
is the command to be parsed by the actor. The following is a valid command
OBSERVER.john 17 sop status
When an actor is commanding another actor we normally write the program.client
as actor.actor
, for example
sop.sop 5 guider decenter on
Tron receives the message, reformats the header and sends it to the actor as
commander_id message_id command_string
where commander_id
is a number identifying the commander and message_id
is a message ID that does not need to be the same as the commanded message_id
.
Replies#
While processing the command, the actor will usually have to output replies back to the hub. The format of the replies is
user_id message_id message_code reply_data
where:
Messages are ASCII.
user_id
is the ID number of the user that sent the command that triggered this reply. Note that this is not the same as thecommander_id
from the command, but the internal ID assigned to the connection socket from which the command came. In most cases where there is a single connection to the actor this is irrelevant. Use 0 to broadcast to all connected users.message_id
is the ID number of the message that triggered this reply. Use 0 if the reply is unsolicited (i.e. not in response to any command).message_code
is a one-character message type code.reply_data
is the message data in keyword-value format. Multiple keywords can be output in the same reply as long as they are separated by semi-colons.
For example, if SOP is replying to OBSERVER.john 17 sop status
from Tron, and assuming that Tron is connected to sop
with user_id=1
, it could say
1 78 i lamps_on=true; ffs="closed"
Note that the message_id=78
does not match the original message_id=17
. This is an internally assigned, unique message_id
that Tron uses to communicate with the actor.
Tron then receives the reply, changes the header to match the original format and then sends it back to all the connected users
OBSERVER.john 17 sop i lamps_on=true; ffs="closed"
A note of caution#
Tron is very flexible and the protocol defined above is only one of the several that it can implement. Different actors may even have different implementations and Tron can configure each one of them independently. The above is, however, the standard followed by most SDSS actors and the one we recommend. When declaring a new Nub in Tron, the following configuration will produce the desired effect
def start(poller):
cfg = Misc.cfg.get(g.location, 'actors', doFlush=True)[name]
stop()
initCmds = ('ping',
'status')
safeCmdsList = ['info', 'ping', 'version', 'status']
safeCmds = r'^\s*({0})\s*$'.format('|'.join(safeCmdsList))
d = ASCIIReplyDecoder(cidFirst=True, debug=1)
e = ASCIICmdEncoder(sendCommander=False, useCID=False, debug=1)
nub = SocketActorNub(poller, cfg['host'], cfg['port'],
name=name, encoder=e, decoder=d,
grabCID=True, initCmds=initCmds,
safeCmds=safeCmds, needsAuth=True,
logDir=os.path.join(g.logDir, name),
debug=1)
hub.addActor(nub)
def stop():
n = hub.findActor(name)
if n:
hub.dropActor(n)
del n
References#
https://www.apo.nmsu.edu/35m_operations/ICCDesign/#Commands (the command header described here is the one used by the APO 3.5-m Tron)
https://trac.sdss3.org/wiki/Ops/Core [private wiki]
CLU Legacy actor#
CLU provides its own implementation of the above protocol via the BaseLegacyActor
class. Although the internals are different, the behaviour for the user should be exactly the same as with the new-style AMQPActor
class (e.g., write
and send_command
have the same interface). The LegacyActor
class provides the actor functionality along with the usual Click-based parsing.
When the actor is run, it starts a server which is an instance of TCPStreamServer
. Optionally, it creates a client connection to Tron that can be accessed over the tron
attribute. When a new user connects to the server, a callback is issued to new_user
, which adds the transport to the list of users and outputs some information to the new user. New commands are handled by the new_command
callback, which parses the command and creates a Command
instance which is then sent to parse_command
.
Keyword parsing#
CLU provides tracking of actor models through Tron. The actors for which models need to be tracked must be specified when starting the actor
with the model_names
list. The models and their values can be accessed via the actor.models
parameter.
Internally the parsing of the keywords received from Tron uses the opscore code (opscore does not need to be installed, the code is now part of CLU and migrated to Python 3) and the model must be defined as part of the actorkeys
product.
Although the internals of parsing and validation are significantly different, the TronModel
class behaves exactly as Model
and the models follows the same structure described in The keyword model.
JSONActor#
CLU also provides a simple JSONActor
which accepts commands following almost the same format as legacy actors for input commands but replies with a JSON string. This makes the replies more verbose and less human-readable but much more easy to parse, since they can be interpreted by just calling json.loads
or the equivalent JSON loading routing for any programming language. JSONActor
is thus useful for devices that will be commanded by an actor but not directly replying to Tron or a user.
JSONActor
is an implementation of TCPBaseActor
using a Click parser.