Virtual devices

Virtual devices let you present a Python-implemented device to the rest of the LPF2 stack — most often to expose custom hardware to a LEGO app through lpf2.hub_emulation, or to synthesise a child device behind a port expander.

The pieces

  • port — an LPF2 port whose transport is a Python-side device. Behaves like any other lpf2.port.

  • device — the emulated device itself. Subclass to implement behaviour; unbound instances just log every call.

  • lpf2.device_descriptor — declares the modes / versions the emulated device advertises.

Building a descriptor

import lpf2

d = lpf2.device_descriptor()
d.fwVersion = lpf2.version()
d.fwVersion.Major = 1
d.hwVersion = lpf2.version()
d.hwVersion.Major = 1

m = lpf2.mode()
m.name = "PWR"
m.min, m.max = -100, 100
m.SImin, m.SImax = -100, 100
m.PCTmin, m.PCTmax = -100, 100
m.format = 0            # int8
m.dataSets = 1
m.outMapping = 0x01     # accepts writes
d.modes = [m]

Subclassing device

Override any of startPower(), startSpeed(), writeData(), setMode(), etc. Calling super().method(...) invokes the C++ default (safely — the trampoline blocks re-entry into Python), so use it for logging / default behaviour:

class MyMotor(lpf2.virtual.device):
    def startPower(self, pw):
        print("MyMotor.startPower", pw)
        super().startPower(pw)

Attaching and running

Both port and device add themselves to the firmware update registry when constructed, so the C main loop drives them automatically — no update() calls from Python are needed (see Automatic polling).

port = lpf2.virtual.port()
dev = MyMotor(d)
port.attachDevice(dev)

# Optional write-intercept callback (fires before default logging):
dev.setWriteDataCallback(lambda mode, data, user: print(mode, data))

# Push a new input sample and fire value-change callbacks:
dev.setModeData(mode=0, buf=b"\\x2a")

Then plug the port into whatever consumes it — either the local port manager (rare) or lpf2.hub_emulation (typical):

emu = lpf2.hub_emulation()
emu.setName("MyHub")
emu.attachPort(0, port)
emu.start()

GC pitfalls

The C++ side holds the port and descriptor; the port holds the device. Anchor the Python objects on your side too — otherwise the GC may free them:

self.port = port
self.dev = dev
self.desc = d