控制 rs-232 接收器,就像它理解更新的 IP 协议一样

控制 rs-232 接收器,就像它理解更新的 IP 协议一样

我想在 Linux 机器上设置一项服务,作为我的旧 RS-232 控制 Onkyo 接收器和本地网络之间的桥梁。到目前为止,我可以使用 socat 来讨论它的一种方法:

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

这让我可以更改音量、源等设置,但是返回确认更改的回复缺少一个简单的字符串,较新的配备以太网的接收器在其回复中包含该字符串。因此,我无法使用它来控制使用当前电话应用程序的接收器,这些应用程序期望启用以太网的单元提供的响应。

有没有办法让 socat 将附加字符串作为响应的一部分包含在内,或者我可以在某些代码的两侧获取 socat 的两个实例,以决定何时何地向消息添加额外的字符串?

此 Excel 表中描述了适用于较旧 RS-232 和较新 IP 方法的 Onkyo 协议(如果有帮助的话): http://blog.siewert.net/files/ISCP%20AV%20Receiver%20v124-1.xls

所有各种现代安桥控制应用程序发送的自动检测请求“!xECNQSTN”,期望得到如下回复:“!1ECNTX-NR609/60128/DX”并且该请求在每次状态更改(例如音量增大,音量减小)后发生等等,所以看起来我需要做一些事情,比如让两个 socat 实例运行,并在它们之间运行一些逻辑。

我总是可以买一个新的现代接收器,但这会更令人满意:o)

非常欢迎任何如何做到这一点的想法!

答案1

你可能需要一些更复杂的东西,比如用Python,因为以太网协议似乎有一些二进制文件,但首先你可以使用bash脚本,比如~/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

通过以下方式在每个连接上从 socat 启动脚本:

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

你应该尽量不要成为这个问题的根源。如果您需要 for ttyUSB0,则暂时将其 chown 或将自己放入正确的组(拨出?)以进行访问。

该脚本在每个连接上运行,并且为了调试,使用/dev/tty运行它的位置作为 stderr。它将串行端口打开为 fd 3。它将以“EOF”字符 0x1a 结尾的输入行读取到 中cmd,然后将其写入串行端口,并将您给出的示例字符串写入标准输出(即以太网)。

您必须识别您收到的输入命令,将其转换为等效的 rs232 协议并回复。

答案2

类似的最大问题是,如果协议是异步的(有时任一端都可以发送),您将不得不做一些事情socat并围绕它构建一个事件循环select(2)能够读取发送者的信息。这需要一些实际的编程语言(Python、Perl?),并且可能需要一些时间来适应。

但是,如果协议是同步的(任何时候只有一方可以通话),那么您可以使用一次从一端读取​​的程序。程序需要解释协议以知道哪一方应该在任何给定点进行通话,否则它可能会陷入等待来自错误端的输入,而不会通过任何进一步的写入。

要从网络与您的程序通信,您可以运行它socat tcp-l:60128,reuseaddr exec:/path/to/my_filter_prog(或在程序本身中实现网络套接字)。对于另一端,可以socat从程序中调用另一个程序与串行端口通信,或者打开串行端口直接从程序中。

模拟 Bash 脚本可能看起来像这样,用于coproc打开两个管道到另一个管道socat以与串行端口通信。 (请注意,我并没有真正查看实际的协议描述。)

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        

答案3

我采纳了你们的建议并尝试了其他方法。最后,我决定将其作为网页,因为这样我就可以添加除音量和开/关之外的其他功能。我非常依赖从多个来源拼凑而成的大量零碎信息,所以要感谢他们所有人。我将node.js与sockets.io一起使用,经过一番尝试和错误后,得到了一些有效的东西,带有来自设备的反馈(因此它可以在页面加载时初始化其状态),并且不会增量地重复嵌套回复(即花了一些时间弄清楚,因为直到几天前我对节点或套接字一无所知!)就是这样。对于真正了解这些东西的人来说可能不是很漂亮,但它似乎做了我想要的!要使用它,请安装node并使用以下命令运行node.js文件:nodejs main.js 将index.html和style.css文件放在名为“public”的子目录中(相对于node.js所在的文件夹)(不带引号)。然后将浏览器指向主机(运行 main.js),并将端口号附加到 url(本例中为 :8080)。

顺便说一下,这适用于 Onkyo TX-SR804,但也应该适用于他们的其他 RS-232 控制接收器,使用 RS-232 转 USB 适配器(亚马逊便宜几美元)。

这是node.js 文件:

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");

这是您将浏览器指向的 HTML index.html 文件。它应该位于名为 public 的文件夹中,该文件夹是包含 node.js 的文件夹的子文件夹。当您将浏览器指向运行 Node.js 的服务器时,请包含端口号(在本例中为 8080)

<!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>

最后是 style.css 文件。应放置在与index.html 文件相同的文件夹中。

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; 
}

网页截图:

相关内容