Creación de un plugin sencillo para generar canales de smartCORE con
El plugin remoto más sencillo (y práctico) que se puede escribir para smartCORE es uno que simplemente genera sus propios datos sin influencia externa. En este ejemplo creamos un plugin que genera tres señales de función diferentes: una onda senoidal, una onda cuadrada y una onda en diente de sierra.
Configuración
En primer lugar creamos una configuración para el módulo smartCORE que define las señales que generamos. Para emitir señales a smartCORE, debemos añadir a nuestra configuración la clave "producerChannels". Contiene una lista de canales con nombre, tipo de datos y unidad física configurados. Para los nombres de nuestras señales elegimos "remote.test.[sine,square,sharktooth]" y como tipo de datos elegimos "float". Aquí también podríamos usar "int" o "double", según lo que requieran los datos generados. No indicamos ninguna unidad física, ya que estos canales generan números arbitrarios para nuestros fines de prueba.
{
"factory": "remote",
"module": "remote",
"config": {
"port": 61616,
"localhost": false,
"producerChannels": [{
"name": "remote.test.sine",
"dataType": "float"
},
{
"name": "remote.test.square",
"dataType": "float"
},
{
"name": "remote.test.sharktooth",
"dataType": "float"
}
]
}
}
Implementación
En nuestro plugin debemos prepararnos para ejecutar el bucle principal. Primero establecemos una conexión con smartCORE y comprobamos si se está ejecutando.
# Parse command line arguments
parser = argparse.ArgumentParser(description='Generates test signals and outputs them to smartCORE channels')
parser.add_argument('--port', dest='port', default=61616, type=int, required=False)
parser.add_argument('--addr', dest='addr', default='127.0.0.1', type=str, required=False)
args = parser.parse_args()
# establish connection to smartCORE
addr = (args.addr, args.port)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.settimeout(2.0)
buffer = packetHeader(CommandType.LifeSignRequest)
buffer += msgpack.packb({})
sock.sendto(buffer, addr)
# wait to receive a reply
received = sock.recv(1500)
# needs try errorhandling
header = header_from_buffer(received[:HEADER_SIZE])
# check that reply has the correct header
if header.type != CommandType.LifeSignResponse.value:
# TODO errorhandling
raise NotImplementedError
buf = BytesIO()
buf.write(received[HEADER_SIZE:])
buf.seek(0)
unpacker = msgpack.Unpacker(buf, raw=False)
# ensure smartCORE is running
for unpacked in unpacker:
if unpacked["smartcore-state"] != "Running":
raise RuntimeError("smartcore is not running")
A continuación solicitamos la lista de canales a los que tenemos acceso. Debería contener los canales que indicamos en nuestra configuración.
# Request channel list
buffer = packetHeader(CommandType.ChannelListRequest)
buffer += msgpack.packb({})
sock.sendto(buffer, addr)
# TODO: needs better read without cutoff
received = sock.recv(1500)
# TODO: needs try errorhandling
header = header_from_buffer(received[:HEADER_SIZE])
# check that reply has correct header
if header.type != CommandType.ChannelListResponse.value:
# TODO errorhandling
raise NotImplementedError
buf = BytesIO()
buf.write(received[HEADER_SIZE:])
buf.seek(0)
unpacker = msgpack.Unpacker(buf, raw=False)
for unpacked in unpacker:
channels = unpacked['c']
Además, creamos un diccionario (Dict) que asigna los nombres de canal a sus índices. Lo utilizamos para enviar nuestros datos a los canales de smartCORE correctos.
# map channel names to their indices
indexDict = {}
for channel in channels:
indexDict[channel['n']] = channel['i']
print(indexDict)
Ahora que hemos dejado atrás la estructura básica, llega la parte divertida. Creamos un bucle infinito y lo envolvemos con una instrucción try-catch para poder salir del bucle mediante una entrada de teclado (CTRL + C). Dentro del bucle creamos tres variables para nuestros tres canales. Sine, que toma el seno del tiempo actual en ms; Square, que alterna cada dos segundos entre 6 y 0; y Sharktooth, que toma el módulo del tiempo actual.
while True:
# Update signal generators
sine = math.sin(int(time.time() * 1_000))
square = 6 if int(time.time()) % 4 >= 2 else 0
sharktooth = time.time() % 51
Una vez que nuestros datos están listos, comenzamos a crear un paquete WriteSamplesRequest, que consta de la cabecera WriteSamplesRequest y una carga útil con nuestros datos. La carga útil consta de un campo "acknowledge" "a" y una lista de canales "c". Los propios canales constan de un índice "i", un valor "v" y una marca de tiempo "t". Rellenamos estos campos con sus índices, los valores actuales y la hora actual.
buffer = packetHeader(CommandType.WriteSamplesRequest)
payload = { "a": "some_placeholder",
"c": [
{
"i": indexDict["remote.test.sine"],
"v": sine,
"t": int(time.time() * 1_000)
},
{
"i": indexDict["remote.test.square"],
"v": square,
"t": int(time.time() * 1_000)
},
{
"i": indexDict["remote.test.sharktooth"],
"v": sharktooth,
"t": int(time.time() * 1_000)
}
]
}
# print(payload)
sys.stdout.flush()
buffer += msgpack.packb(payload)
sock.sendto(buffer, addr)
# TODO: needs better read without cutoff
Una vez que hayamos enviado el paquete y recibido de vuelta el "paquete Ack", deberíamos poder ver nuestras señales en optiCONTROL o en optiCLOUD.