2

I would like to set up a service on a linux box that acts as a bridge between my old RS-232 controlled Onkyo receiver and my local network. So far I can talk one way to it using socat:

sudo socat tcp-l:60128,reuseaddr,fork file:/dev/ttyUSB0,nonblock,raw,echo=0,crnl,waitlock=/ttyUSB0.lock &

That lets me change settings like volume, source etc., but the reply that comes back to acknowledge the change is missing a simple string that newer ethernet equipped receivers include in their reply. As a result I can't use this to control the receiver using current phone apps that expect the response that the ethernet enabled units provide.

Is there a way to get socat to include the additional string as part of the response or can I get two instances of socat either side of some code which decides when and where to add extra strings to the message?

The Onkyo protocol for both the older RS-232 and the newer IP methods are described in this excel sheet if that helps: http://blog.siewert.net/files/ISCP%20AV%20Receiver%20v124-1.xls

The auto-detect request "!xECNQSTN" that all the various modern onkyo controlling apps send, expects to get a reply like: '!1ECNTX-NR609/60128/DX' And that request happens after every state change like volume up, volume down, etc, so it looks like I need to do something like having two instances of socat running with some logic in between.

I could always just get a new modern receiver but this would be way more satisfying :o)

Any ideas how to do this are very welcome!

  • What relation an rs232 port or device behind that port has to IP protocol stack or its version? – Serge Sep 12 '16 at 6:49
  • Thanks @serge Onkyo use something called ISCP over ethernet. If I understand correctly, it is simply an ethernet way to use the commands set they had established previously for RS-232. – DrBubbles Sep 13 '16 at 6:14
0

You'll probably need something more sophisticated, say in Python, as the ethernet protocol seems to have some binary in it, but to start off you could use a bash script, say, ~/myonkyo:

#!/bin/bash
exec 2>/dev/tty
set -x
tty=/dev/ttyUSB0
stty -F $tty raw clocal -echo
exec 3<>$tty
echo "connection" >&2
while IFS= read -r -d $'\x1a' cmd
do  echo "$cmd" >&3
    echo "!1ECNTX-NR609/60128/DX"
done

Start the script from socat on each connection by:

$ socat tcp-l:60128,reuseaddr SYSTEM:~/myonkyo

You should try not being root for this. If you need to be for ttyUSB0, then chown it temporarily or put yourself in the right group (dialout?) for access.

The script runs on each connection, and for debug uses the /dev/tty where you run it as stderr. It opens the serial port as fd 3. It reads the input line ending with the "EOF" character 0x1a into cmd, then writes that to the serial port, and writes to stdout, i.e. the ethernet, the example string you gave.

You are going to have to recognise which input command you are getting, convert it to the rs232 protocol equivalent and reply.

  • Thank you @meuh this gives me a good starting point, definitely can see a way forward with this. I'll try with dialout group; I had tried avoiding root before by becoming part of one group or another but still couldn't use /dev/ttyUSB0 so got lazy and reverted to root. Will have another go and try to do the right thing :o) – DrBubbles Sep 13 '16 at 6:22
0

The biggest issue with something like that is that if the protocol is asynchronous (there are times when either end can send), you'll have to do what socat does and build an event loop around select(2) to be able to read whoever happens to be sending. That would require some actual programming language (Python, Perl?) and might take some getting used to.

However, if the protocol is synchronous (at any time only one party can talk), then you could do with a program that reads from one end at a time. The program will need to interpret the protocol to know which party should be talking at any given point, otherwise it could get stuck waiting for input from the wrong end, without passing through any further writes.

To talk to your program from the network, you could run it with socat tcp-l:60128,reuseaddr exec:/path/to/my_filter_prog (or implement the network socket in the program itself.) And for the other end, either call another socat from the program to talk to the serial port, or open the serial port directly from the program.

A mock up Bash script could look something like this, using coproc to open two pipes to another socat to talk to the serial port. (Note that I didn't really look at the actual protocol description.)

coproc socat - file:/dev/ttyUSB0,nonblock,raw,echo=0,crnl,waitlock=/ttyUSB0.lock
serin=${COPROC[0]}
serout=${COPROC[1]}

# assume we have stdin/stdout connected to the other end,
# as with socat tcp-listen:... exec:./this

while true ; do
    # read a command from stdin, pass it through to serial
    read -r cmd 
    echo "$cmd" >&$serout
    # do we need to read and pass another line at this point?
    # might depend on the command, but we need to know that.

    # read the reply and pass it through
    read -r reply <&$serin
    # add/modify something based on the command or the reply?
    echo "$reply" 
    if [ "$cmd" = "!xECNQSTN" ] ; then
        echo "!1ECNTX-NR609/60128/DX"
    fi
done        
  • Normally the communication is synchronous, but if someone uses the remote or turns a dial on the receiver, the receiver will send an update message, so then it becomes asynchronous. If I hide the remote and lock the receiver in box then I might be able to ignore the asynchronous part :o) Then I can try using your solution, thank you! I will try this first and then maybe expand it in python with an event loop. – DrBubbles Sep 14 '16 at 3:50
0

I took both of your advice and tried doing it some other way. In the end I decided to do it as a web page as I could then add additional functionality beyond volume and on/off. I relied heavily on lots of bits and pieces cobbled together from several sources so thanks to all of them. I used node.js with sockets.io and after a bit of trial and error got something that works, with feedback from the device (so it can initialize its state when the page loads) and which doesn't incrementally duplicate nested replies (that took some figuring out as I knew nothing about node or sockets up until a few days ago!) Here it is. Probably not very pretty to anyone who knows this stuff properly, but it appears to do what I want! To use it, install node and run the node.js file using the command: nodejs main.js Put the index.html and style.css files in a sub directory (relative to the folder with node.js) called "public" (without the quotes). Then point your browser at the host (running main.js) with the port number appended to the url ( :8080 in this case).

By the way, this is for an Onkyo TX-SR804 but should work for their other RS-232 controlled receivers too, using an RS-232 to USB adaptor (a few bucks off amazon).

This is the node.js file:

var express = require('express');
app         = express();
server      = require('http').createServer(app);
io          = require('socket.io').listen(server);

var SerialPort = require("serialport")
var serialPort = new SerialPort("/dev/ttyUSB0", { 
        baudRate: 9600,
        dataBits: 8,
        parity: 'none',
        stopBits: 1
        }
    );

server.listen(8080);
app.use(express.static('public'));             
var paramVal = 0;
var countRep = 0;
var countSend = 0;
var buf = new Buffer(16);
var global_socket;

io.sockets.on('connection', function (socket) {
    global_socket = socket;
    global_socket.on('toOnkyo', function (data) {
        paramVal = data.value;
        buf.write(paramVal, "utf-8");
        serialPort.write(buf);
        console.log(paramVal.toString().substr(0,7) + " (" + parseInt(paramVal.toString().substr(5,2),16) + ")\r\n");               
        global_socket.emit('toOnkyo', {value: paramVal});   
        console.log('new'+paramVal);
        countSend=countSend+1;
        console.log('count send '+ countSend);
        }
    );
    }
);
serialPort.on('data', function(data) {
    console.log('data received: ' + data.toString().substr(0,7) + " (" + parseInt(data.toString().substr(5,2),16) + ")");
    global_socket.emit('onkyoReply', {value: data.toString().substr(0,7)});
    countRep=countRep+1;
    console.log('count '+ countRep);
    }
);
console.log("running");

This is the HTML index.html file that you point your browser at. It should be in a folder called public, a sub folder of the one with node.js in it. when you point your browser at the server running node.js, include the port number (8080 in this case)

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
        <title>Onkyo Controller</title>
        <meta name="viewport" content="width=400px" />
        <script src="socket.io/socket.io.js"></script>
        <link rel="stylesheet" href="style.css">
  </head>
  <body>
<!--
Sent:       <span id="sliderVolText"></span><br>
Reply:      <span id="replyTextHex"></span>
(Decimal:   <span id="replyText10"></span>)<br>
Mode:       <span id="modeText"></span><br>
PowerText:  <span id="powerText"></span><br>
Power:      <span id="power"></span><br>
onoffText:  <span id="onoffText"></span><br>
onoff:      <span id="onoff"></span>  
-->

<span id="sliderVolText" style="display:none"></span>
<span id="replyTextHex" style="display:none"></span>
<span id="replyText10" style="display:none"></span>
<span id="modeText" style="display:none"></span>
<span id="sourceText" style="display:none"></span>
<span id="powerText" style="display:none"></span>
<span id="power" style="display:none"></span>
<span id="onoffText" style="display:none"></span>
<span id="onoff" style="display:none"></span>  

    <script>
    function setCheckedValue(radioObj, newValue) {
        if(!radioObj)
            return;
        var radioLength = radioObj.length;
        if(radioLength == undefined) {
            radioObj.checked = (radioObj.value == newValue.toString());
            return;
        }
        for(var i = 0; i < radioLength; i++) {
            radioObj[i].checked = false;
            if(radioObj[i].value == newValue.toString()) {
                radioObj[i].checked = true;
            }
        }
    }
    </script>



    <form class="onoffswitch" >
        <input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="myonoffswitch"  onclick="showOnoff(checked)">
        <label class="onoffswitch-label" for="myonoffswitch">
        <span class="onoffswitch-inner"></span>
        <span class="onoffswitch-switch"></span>
        </label>
    </form>
<!--
    <form name="powerForm" method="get" action="" onsubmit="return false;">
        <p>   <label for="power0"><input type="radio" value="0x00" name="powerForm" id="power0" onclick="showPower(this.value)"> Off</label>
        &nbsp;<label for="power1"><input type="radio" value="0x01" name="powerForm" id="power1" onclick="showPower(this.value)"> On</label>
    </form>
-->
    <form name="modeForm" method="get" action="" onsubmit="return false;">
        <p>   <label for="mode0"><input type="radio" value="0x00" name="modeForm" id="mode0" onclick="showMode(this.value)"> Stereo</label>
        &nbsp;<label for="mode1"><input type="radio" value="0x01" name="modeForm" id="mode1" onclick="showMode(this.value)"> Direct</label>
        &nbsp;<label for="mode2"><input type="radio" value="0x0C" name="modeForm" id="mode2" onclick="showMode(this.value)"> All Ch stereo</label>
        &nbsp;<label for="mode3"><input type="radio" value="0x42" name="modeForm" id="mode3" onclick="showMode(this.value)"> THX Cinema</label>
        &nbsp;<label for="mode4"><input type="radio" value="0x84" name="modeForm" id="mode4" onclick="showMode(this.value)"> PLllx THX Cinema</label>
        &nbsp;<label for="mode5"><input type="radio" value="0x11" name="modeForm" id="mode5" onclick="showMode(this.value)"> Pure</label>
    </form>
<br>

<form name="sourceForm" method="get" action="" onsubmit="return false;">
        <p>   <label for="source0"><input type="radio" value="0x00" name="sourceForm" id="source0" onclick="showSource(this.value)"> Computer</label>
        &nbsp;<label for="source2"><input type="radio" value="0x24" name="sourceForm" id="source2" onclick="showSource(this.value)"> FM radio</label>
<!--
        &nbsp;<label for="source1"><input type="radio" value="0x01" name="sourceForm" id="source1" onclick="showSource(this.value)"> Video 2</label>
        &nbsp;<label for="source3"><input type="radio" value="0x26" name="sourceForm" id="source3" onclick="showSource(this.value)"> Tuner</label>
-->
        </form>
<br>


    <form name="slideForm" method="get" action="" onsubmit="return false;">
        <input type="range" id= "inputSlider" min="0" max="100" value="vol" step="1" oninput="showVolume(this.value)" />
    </form>
    <br>
    <div class="results"></div>
    <script type="text/javascript">
//      function toggle(checked) {
//        var elm = document.getElementById('checkbox');
//        if (checked != elm.checked) {
//          elm.click();
//        }
//      }

        var socket = io.connect();
        var ctrlType = "";
            socket.on('toOnkyo', function (data) {
                ctrlType = data.value.toString().substr(2,3);
                if (ctrlType == "MVL" && !(data.value.toString().substr(5,4)=="QSTN")){
                    document.getElementById("inputSlider").value =  parseInt(data.value.toString().substr(5,2),16);
                    document.getElementById("sliderVolText").innerHTML = data.value;
                }
                if (ctrlType == "LMD" && !(data.value.toString().substr(5,4)=="QSTN")){
                    document.getElementById("mode").value =  parseInt(data.value.toString().substr(5,2),16);
                    document.getElementById("modeText").innerHTML = data.value;
                }
                if (ctrlType == "PWR" && !(data.value.toString().substr(5,4)=="QSTN")   ){
                    document.getElementById("power").value =  parseInt(data.value.toString().substr(5,2),16);
                    document.getElementById("powerText").innerHTML = data.value;
                }
                if (ctrlType == "PWR" && !(data.value.toString().substr(5,4)=="QSTN")   ){
                    document.getElementById("onoff").value =  parseInt(data.value.toString().substr(5,2),16);
                    document.getElementById("onoffText").innerHTML = data.value;
                }
                if (ctrlType == "SLI" && !(data.value.toString().substr(5,4)=="QSTN")){
                    document.getElementById("source").value =  parseInt(data.value.toString().substr(5,2),16);
                    document.getElementById("sourceText").innerHTML = data.value;
                }
            });
            socket.on('onkyoReply', function (data) {
                var done = false;
                ctrlType = data.value.toString().substr(2,3);
                document.getElementById("replyTextHex").innerHTML = data.value;
                document.getElementById("replyText10").innerHTML = parseInt(data.value.toString().substr(5,2),16);
                if (ctrlType == "LMD"){
                    setCheckedValue(document.forms['modeForm'].elements['modeForm'],"0x"+data.value.toString().substr(5,2));
                }
                if (ctrlType == "SLI"){
                    setCheckedValue(document.forms['sourceForm'].elements['sourceForm'],"0x"+data.value.toString().substr(5,2));
                }
                if (ctrlType == "PWR"){
                    var val = parseInt(data.value.toString().substr(5,2),16);
//                  setCheckedValue(document.forms['powerForm'].elements['powerForm'],"0x"+data.value.toString().substr(5,2));
                    document.getElementById("myonoffswitch").checked = (data.value.toString().substr(6,1) != 0);

//                  console.log(ctrlType);
//                  If (val == 1) {
//                      document.getElementById("myonoffswitch").checked = true;
//                  }
//                  If (data.value.toString().substr(6,1)=='0') {
//                      document.getElementById("myonoffswitch").checked = false;
//                  } else {
//                      document.getElementById("myonoffswitch").checked = true;
//                  };
//                  document.getElementById('myonoffswitch').click();
                }
                if (ctrlType == "MVL" && done == false){
                    document.getElementById("inputSlider").value = parseInt(data.value.toString().substr(5,2),16);
                    document.querySelector('.results').innerHTML = parseInt(data.value.toString().substr(5,2),16);
                    done = true;                        
                }
            });

            function showVolume(newValue) {                     
                document.getElementById("sliderVolText").innerHTML="\!1MVL"+("0" + Number(newValue).toString(16)).slice(-2)+"\r\n";
                socket.emit('toOnkyo', { value: "\!1MVL"+("0" + Number(newValue).toString(16)).slice(-2)+"\r\n" });
            }

            function showMode(newValue) {
                document.getElementById("modeText").innerHTML="\!1LMD"+("0" + Number(newValue).toString(16)).slice(-2)+"\r\n";
                socket.emit('toOnkyo', { value: "\!1LMD"+("0" + Number(newValue).toString(16)).slice(-2)+"\r\n" });
            }

            function showSource(newValue) {
                document.getElementById("sourceText").innerHTML="\!1SLI"+("0" + Number(newValue).toString(16)).slice(-2)+"\r\n";
                socket.emit('toOnkyo', { value: "\!1SLI"+("0" + Number(newValue).toString(16)).slice(-2)+"\r\n" });
            }

//          function showPower(newValue) {
//              document.getElementById("powerText").innerHTML="\!1PWR"+("0" + Number(newValue).toString(16)).slice(-2)+"\r\n";
//              socket.emit('toOnkyo', { value: "\!1PWR"+("0" + Number(newValue).toString(16)).slice(-2)+"\r\n" });
//          }

            function showOnoff(newValue) {
                document.getElementById("onoffText").innerHTML="\!1PWR"+("0" + Number(newValue).toString(16)).slice(-2)+"\r\n";
                socket.emit('toOnkyo', { value: "\!1PWR"+("0" + Number(newValue).toString(16)).slice(-2)+"\r\n" });
            }

            socket.emit('toOnkyo', { value: "\!1LMDQSTN"+"\r\n" });
            socket.emit('toOnkyo', { value: "\!1MVLQSTN"+"\r\n" });
            socket.emit('toOnkyo', { value: "\!1PWRQSTN"+"\r\n" });
            socket.emit('toOnkyo', { value: "\!1SLIQSTN"+"\r\n" });

    </script>
  </body>
</html>

Finally the style.css file. Should be placed in the same folder as the index.html file.

body {
    text-align: center;
    margin-top: 50px;
    background: #50D0A0;
}

input[type=range]{
    -webkit-appearance: none;
    width: 80%;
}

input[type=range]::-webkit-slider-runnable-track {
    height: 10px;
    background: #ddd;
    border: none;
    border-radius: 3px;
}

input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none;
    border: none;
    height: 32px;
    width: 32px;
    border-radius: 50%;
    background: /* goldenrod */ #34A7C1;
    margin-top: -12px;
}

input[type=range]:focus {
    outline: none;
}

input[type=range]:focus::-webkit-slider-runnable-track {
    background: #ccc;
}
.radioLeft
{
    text-align:left;
}

.onoffswitch {
    position: relative; width: 90px;
    -webkit-user-select:none; -moz-user-select:none; -ms-user-select: none;

    left: 50%;
    margin-right: -50%;
    transform: translate(-50%, -50%) 

    }
.onoffswitch-checkbox {
    display: none;
}
.onoffswitch-label {
    display: block; overflow: hidden; cursor: pointer;
    border: 2px solid #999999; border-radius: 20px;
}
.onoffswitch-inner {
    display: block; width: 200%; margin-left: -100%;
    transition: margin 0.3s ease-in 0s;
}
.onoffswitch-inner:before, .onoffswitch-inner:after {
    display: block; float: left; width: 50%; height: 30px; padding: 0; line-height: 30px;
    font-size: 14px; color: white; font-family: Trebuchet, Arial, sans-serif; font-weight: bold;
    box-sizing: border-box;
}
.onoffswitch-inner:before {
    content: "ON";
    padding-left: 10px;
    background-color: #34A7C1; color: #FFFFFF;
}
.onoffswitch-inner:after {
    content: "OFF";
    padding-right: 10px;
    background-color: #EEEEEE; color: #999999;
    text-align: right;
}
.onoffswitch-switch {
    display: block; width: 18px; margin: 6px;
    background: #FFFFFF;
    position: absolute; top: 0; bottom: 0;
    right: 56px;
    border: 2px solid #999999; border-radius: 20px;
    transition: all 0.3s ease-in 0s; 
}
.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner {
    margin-left: 0;
}
.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch {
    right: 0px; 
}

Screen shot of web page:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.