我想在 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>
<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>
<label for="mode1"><input type="radio" value="0x01" name="modeForm" id="mode1" onclick="showMode(this.value)"> Direct</label>
<label for="mode2"><input type="radio" value="0x0C" name="modeForm" id="mode2" onclick="showMode(this.value)"> All Ch stereo</label>
<label for="mode3"><input type="radio" value="0x42" name="modeForm" id="mode3" onclick="showMode(this.value)"> THX Cinema</label>
<label for="mode4"><input type="radio" value="0x84" name="modeForm" id="mode4" onclick="showMode(this.value)"> PLllx THX Cinema</label>
<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>
<label for="source2"><input type="radio" value="0x24" name="sourceForm" id="source2" onclick="showSource(this.value)"> FM radio</label>
<!--
<label for="source1"><input type="radio" value="0x01" name="sourceForm" id="source1" onclick="showSource(this.value)"> Video 2</label>
<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;
}