최신 IP 프로토콜을 이해하는 것처럼 rs-232 수신기를 제어합니다.

최신 IP 프로토콜을 이해하는 것처럼 rs-232 수신기를 제어합니다.

나는 기존 RS-232로 제어되는 Onkyo 수신기와 로컬 네트워크 사이의 브리지 역할을 하는 Linux 상자에 서비스를 설정하고 싶습니다. 지금까지 논의할 수 있는 한 가지 방법은 socat을 사용하는 것입니다.

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

이를 통해 볼륨, 소스 등과 같은 설정을 변경할 수 있었지만 변경 사항을 확인하는 응답에는 최신 이더넷 장착 수신기의 응답에 포함되는 간단한 문자열이 누락되었습니다. 따라서 이더넷 지원 장치의 응답을 기대하는 현재 전화 애플리케이션을 사용하여 수신기를 제어하는 ​​데 사용할 수 없습니다.

socat에 응답의 일부로 추가 문자열을 포함시키는 방법이 있습니까? 아니면 메시지에 추가 문자열을 추가할 시기와 위치를 결정하기 위해 일부 코드의 양쪽에 socat의 두 인스턴스를 얻을 수 있습니까?

이전 RS-232 및 최신 IP 방법에 대한 Onkyo 프로토콜은 이 Excel 시트에 설명되어 있습니다(도움이 되는 경우). http://blog.siewert.net/files/ISCP%20AV%20Receiver%20v124-1.xls

모든 다양한 최신 Onkyo 제어 애플리케이션은 "!1ECNTX-NR609/60128/DX"와 같은 응답을 기대하면서 자동 감지 요청 "!xECNQSTN"을 보내고 상태가 변경될 때마다 요청이 변경됩니다(예: 볼륨 높이기, 볼륨 낮추기 작게) ) 등이 있으므로 두 개의 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 system:~/myonkyo

당신은 이 문제의 원인이 되지 않도록 노력해야 합니다. 가 필요한 경우 ttyUSB0임시로 권한을 부여하거나 액세스할 수 있는 올바른 그룹(다이얼아웃?)에 자신을 넣으세요.

스크립트는 모든 연결에서 실행되며 디버깅 목적으로 /dev/ttystderr로 실행되는 위치를 사용합니다. 직렬 포트를 fd 3으로 엽니다. "EOF" 문자 0x1a 로 끝나는 입력 라인을 읽은 다음 cmd이를 직렬 포트에 쓰고 표준 출력(예: 이더넷)에 제공한 예제 문자열을 씁니다.

수신한 입력 명령을 식별하고 이를 동등한 RS232 프로토콜로 변환한 후 응답해야 합니다.

답변2

socat이와 같은 것의 가장 큰 문제는 프로토콜이 비동기식인 경우(때로는 어느 한쪽 끝에서 보낼 수 있음) 무언가를 수행 하고 이를 중심으로 이벤트 루프를 구축 해야 한다는 것입니다.select(2)발신자의 정보를 읽는 능력. 이를 위해서는 실제 프로그래밍 언어(Python, Perl?)가 필요하며 익숙해지는 데 시간이 걸릴 수 있습니다.

그러나 프로토콜이 동기식인 경우(언제든지 한 당사자만 대화할 수 있음) 한 번에 한쪽 끝에서 읽는 프로그램을 사용할 수 있습니다. 프로그램은 특정 지점에서 어느 쪽이 대화해야 하는지 알기 위해 프로토콜을 해석해야 합니다. 그렇지 않으면 추가 쓰기를 전달하지 않고 잘못된 쪽의 입력을 기다리다가 멈출 수 있습니다.

네트워크에서 프로그램과 통신하려면 프로그램을 실행 socat tcp-l:60128,reuseaddr exec:/path/to/my_filter_prog하거나 프로그램 자체에서 네트워크 소켓을 구현하면 됩니다. 다른 쪽 끝에서는 socat프로그램 내에서 다른 프로그램을 호출하여 직렬 포트와 통신하거나 프로그램 내에서 직접 직렬 포트를 열 수 있습니다.

직렬 포트와 통신하기 위해 coproc두 개의 파이프를 다른 파이프로 여는 시뮬레이션된 Bash 스크립트는 다음과 같습니다 . 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

나는 당신의 조언을 듣고 다른 것을 시도했습니다. 결국 볼륨이나 ON/OFF 외에 다른 기능도 추가할 수 있어서 웹페이지로 만들기로 결정했습니다. 저는 여러 소스에서 수집한 수많은 정보에 크게 의존했습니다. 모두에게 감사드립니다. 저는 Sockets.io와 함께 node.js를 사용하고 있으며 몇 번의 시행착오 끝에 장치의 피드백을 통해 작동하는 것을 얻었고(페이지가 로드될 때 상태를 초기화할 수 있음) 반복되는 중첩된 응답이 많이 증가하지 않습니다. (즉, 며칠 전까지 노드나 소켓에 대해 아무것도 몰랐기 때문에 알아내는 데 시간이 좀 걸렸습니다!) 그게 전부입니다. 이 내용을 잘 아는 사람에게는 예쁘지 않을 수도 있지만, 내가 원하는 대로 되는 것 같습니다! 이를 사용하려면 node를 설치하고 다음 명령으로 node.js 파일을 실행하십시오. nodejs main.js index.html 및 style.css 파일을 "public"이라는 하위 디렉토리(node.js가 있는 파일에 상대적)에 배치하십시오. 클립) (인용 부호 제외). 그런 다음 브라우저에서 호스트(main.js 실행)를 가리키고 URL에 포트 번호를 추가합니다(이 경우: 8080).

그건 그렇고, 이것은 Onkyo TX-SR804에서 작동하지만 RS-232-USB 어댑터(Amazon에서 몇 달러 더 저렴함)를 사용하여 다른 RS-232 제어 수신기에서도 작동해야 합니다.

이것은 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 파일입니다. node.js가 포함된 폴더의 하위 폴더인 public 폴더에 있어야 합니다. 브라우저에서 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; 
}

웹페이지 스크린샷:

관련 정보