Optoma Projector (Serial)

Hey :wave:

I recently inherited a Optoma Themescene HD86 projector which I wanted to control via Home Remote. It’s a few years old and doesn’t have a network port, but rather than opt for IR control I used an IP to RS232 adapter so I could address its serial port over the network. Using this I wrote a basic plugin so Home Remote can turn it on and off, and know its state:

plugin.Name = "OptomaProjector";
plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
plugin.PollingInterval = 2000;
plugin.DefaultSettings = { "Host": "", "Port": "23" };

var tcp = new TCPClient();
var tcpDeviceId = "projector";

function onChangeRequest(device, attribute, value) {
    switch (attribute) {
        case "Switch":
            switch (value) {
                case "On":
                    console.log("Turning On Projector");
                    tcp.send("~0000 1\r");
                case "Off":
                    console.log("Turning Off Projector");
                    tcp.send("~0000 2\r");

function onConnect() {
    var host = plugin.Settings["Host"];
    var port = plugin.Settings["Port"];
    if (host && port) {
        tcp.connect(host, parseInt(port));
        console.log("Connected to " + host + ":" + port);
    else {
        tcp.connect("", 23);

function onDisconnect() {

function onPoll() {
    tcp.send("~00124 1\r");
    var data = tcp.receive({timeout: 1000});
    console.log("message received: " + data);
    var lastResponse = data.trim().split("\r").pop();
    console.log("response: " + lastResponse);
    switch (lastResponse) {
        case "OK0":
            console.log("State is off");
            plugin.Devices[tcpDeviceId].Switch = "Off";
        case "OK1":
            console.log("State is on");
            plugin.Devices[tcpDeviceId].Switch = "On";

function onSynchronizeDevices() {
    var projectorDevice = new Device();
    projectorDevice.Id = tcpDeviceId;
    projectorDevice.DisplayName = "Optoma Projector";
    projectorDevice.Capabilities = ["Switch"];
    projectorDevice.Attributes = [];
    plugin.Devices[projectorDevice.Id] = projectorDevice;

It took a few iterations to get the tcp handling right - it’s very easy to get blocked on tcp.receive(), even with a timeout (doesn’t seem to work?) - but it’s working pretty well at this point. Serial commands are listed in the user manual found here. I took a quick look through the manual of newer projectors and the command codes look the same, so this should work for those too if anyone needs that.

1 Like

Looks good! I’m just curious though, why don’t you want the “receive” function to block?

Generally speaking, blocking & waiting for response data is a good thing. It’s more efficient & generally performs better. Most of the plugins I write operate that way. Some people get confused with how plugin polling works. The Home Remote is not constantly calling this function. It’s a loop. The loop starts by calling “onPoll” & then waits for it to finish. It can take however long it needs to complete. Then it waits for the time period specified in “PollingInterval”. When that expires it again calls “onPoll” & waits for it to complete. This process loops over & over. So you see, it’ll never call “onPoll” while another “onPoll” call is already running. Perhaps “PollingInterval” wasn’t the best name for this setting because it’s really a “polling loop delay”.

One other thing you could consider changing, is moving tcp.send("~00124 1\r"); up into “onConnect”. Many TCP devices automatically send messages whenever the state changes. So it may not be necessary to request status every time in your polling loop. If Optoma doesn’t automatically broadcast state changes on the port then you will need to leave that in there.

Take a look at my multi zone audio plugin. It blocks & waits in the polling loop for messages to be received. It also only sends status request messages in “onConnect”. All the polling loop does is processes incoming messages.

I originally wanted to flush any received data in the buffer before sending new commands, to better reason about the response. I tried calling tcp.receive with a timeout in case there was no data to flush, but it always hung :man_shrugging:

The Home Remote is not constantly calling this function. It’s a loop.

Ah, I see, yes I did not expect this. You are right that the device will send messages when the state changes, and I decided not to handle that - if multiple poll() calls were in flight it would be hard to reason about. I can definitely refactor this to be more efficient after your explanation, thanks :+1:

To flush the data you can use the “available” property to check to see how many bytes are currently in the receive buffer.