Sending basic commands via UDP to Ruida

Hello,
I’m trying to build something like a remote controller for basic interface functions like moving axis, start/stop, frame, etc. The official one is way to expensive for what it does imo. The ACS / RMA app is okayish but not as nice as having a dedicated remote, so I’d like to build it myself.

I’m using an ESP-32 dev board to setup wifi connection to the AP which is connected to the DSP (the ACS app is working flawlessly btw.).
Now I’m stuck at the udp communication. According to this wiki you can use very basic udp commands on port 50207 to talk to the DSP but I have no luck with this.

My code:

#include WiFi.h>
#include WiFiUdp.h>

// network settings
const char* ssid = "**********";
const char* password = "*********";
const IPAddress controllerIP(192, 168, 11, 99);
const int udpSendPort = 50207; // Port for sending
const int udpAckPort = 40207;  // Port for receiving

// pins
const int buttonPins[] = {2, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25}; // 15 Taster
const int statusLedPin = 26; // LED status
const int wifiLedPin = 27; // LED wifi
const int buttonLedPin = 32; // LED button press

WiFiUDP udpSend; 
WiFiUDP udpAck;  

bool waitingForAck = false;
unsigned long lastCommandTime = 0;
const unsigned long ackTimeout = 1000; // Timeout for ACK


bool blinkWifiLed = false;
unsigned long blinkStartTime = 0;
const unsigned long blinkDuration = 50; 

void setup() {
 
  for (int i = 0; i < 15; i++) {
    pinMode(buttonPins[i], INPUT_PULLUP);
  }
  pinMode(statusLedPin, OUTPUT);
  pinMode(wifiLedPin, OUTPUT);
  pinMode(buttonLedPin, OUTPUT);

 
  digitalWrite(statusLedPin, HIGH);

  //Wifi connection
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    digitalWrite(wifiLedPin, HIGH);
    delay(500);
    digitalWrite(wifiLedPin, LOW);
    delay(500);
  }


  digitalWrite(wifiLedPin, HIGH);

  // UDP start
  udpSend.begin(udpSendPort);
  udpAck.begin(udpAckPort); 
}

void loop() {
  //check button press
  for (int i = 0; i < 15; i++) {
    if (digitalRead(buttonPins[i]) == LOW) { // pressed
      sendCommand(i, false); 
      digitalWrite(buttonLedPin, HIGH);
      while (digitalRead(buttonPins[i]) == LOW); // released
      sendCommand(i, true); 
      digitalWrite(buttonLedPin, LOW);
    }
  }

  checkForAck();
  handleWifiLedBlink();

  // send Keep-Alive-package
  static unsigned long lastKeepAlive = 0;
  if (millis() - lastKeepAlive > 1000) {
    sendKeepAlive();
    lastKeepAlive = millis();
  }
}

void checkForAck() {
  if (waitingForAck) {
    // check for ACK package
    int packetSize = udpAck.parsePacket();
    if (packetSize) {
      byte ack = udpAck.read();
      if (ack == 0xCC) {
        Serial.println("ACK received!");
        waitingForAck = false; 
        startWifiLedBlink();  
      }
    }

    // check timeout
    if (millis() - lastCommandTime > ackTimeout) {
      Serial.println("no ACK received!");
      waitingForAck = false; 
    }
  }
}

void sendCommand(int buttonIndex, bool isStop) {
  byte command[3] = {0xA5, 0x00, 0x00};

  // start-command (0x50)
  if (!isStop) {
    command[1] = 0x50;
    switch (buttonIndex) {
      case 0: command[2] = 0x02; break; // +X Down
      case 1: command[2] = 0x01; break; // -X Down
      case 2: command[2] = 0x03; break; // +Y Down
      case 3: command[2] = 0x04; break; // -Y Down
      case 4: command[2] = 0x0A; break; // +Z Down
      case 5: command[2] = 0x0B; break; // -Z Down
      case 6: command[2] = 0x0C; break; // +U Down
      case 7: command[2] = 0x0D; break; // -U Down
      case 8: command[2] = 0x11; break; // Speed
      case 9: command[2] = 0x06; break; // Start/Pause
      case 10: command[2] = 0x09; break; // Stop
      case 11: command[2] = 0x5A; break; // Reset
      case 12: command[2] = 0x00; break; // Frame
      case 13: command[2] = 0x08; break; // Origin
      case 14: command[2] = 0x05; break; // Pulse
    }
  }
  // stop-commands (0x51)
  else {
    command[1] = 0x51;
    switch (buttonIndex) {
      case 0: command[2] = 0x02; break; // +X Up
      case 1: command[2] = 0x01; break; // -X Up
      case 2: command[2] = 0x03; break; // +Y Up
      case 3: command[2] = 0x04; break; // -Y Up
      case 4: command[2] = 0x0A; break; // +Z Up
      case 5: command[2] = 0x0B; break; // -Z Up
      case 6: command[2] = 0x0C; break; // +U Up
      case 7: command[2] = 0x0D; break; // -U Up
     
      default: return; 
    }
  }

  udpSend.beginPacket(controllerIP, udpSendPort);
  udpSend.write(command, 3);
  udpSend.endPacket();

    // start ACK-check 
  waitingForAck = true;
  lastCommandTime = millis();
}

void startWifiLedBlink() {
  blinkWifiLed = true;
  blinkStartTime = millis();
  digitalWrite(wifiLedPin, LOW); 
}

void handleWifiLedBlink() {
  if (blinkWifiLed) {
    if (millis() - blinkStartTime >= blinkDuration) {
      digitalWrite(wifiLedPin, HIGH); 
      blinkWifiLed = false;
    }
  }
}

void sendKeepAlive() {
  byte keepAlive = 0xCE;
  udpSend.beginPacket(controllerIP, udpSendPort);
  udpSend.write(&keepAlive, 1);
  udpSend.endPacket();

}

Wifi is connecting without a problem but I can’t seem to get any commands to the DSP. Do I have to use the 50200/40200 ports and the rd file payload or am I doing something wrong? Any ideas?

#include WiFi.h>
#include WiFiUdp.h>

I am not sure what you are doing but use this instead of unstarted preprocessor directives:

#include <Your_File.h>

I know you already know but it will help if the file gets more complicated and then when using other preprocessor directives, less issue will arise.

Seth

I had the same thought. It’s not so much that the BWK301R is too expensive, its functionality is pretty limited. I was spoiled on CNC with LinuxCNC and a wireless controller with a physical jog dial, a selectable jog speed multiplier, macro buttons that could actuaolly do something, and a readout screen. It was cheaper, too.

I have an alternative route to suggest:
The BWK301R wireless controller for the Ruida comes with a small antenna receiver that plugs into a separate physical plug on the Ruida.

I read it’s RS232 serial. I don’t know its messaging format, but obviously it is capable of jogging, start/stop, frame, and origin set.

I would bet that RS232 actually uses the same command set as the UDP interface, and would be capable of commanding the Ruida and has access to its information. It would be more bandwidth limited, though.

Like I say, I haven’t tried to log the RS232 data stream. If you have a BWK301R remote, it would be easy enough to find the baud rate and sniff the RS232 to log. Then you’d have to pore over it to decode the commands. Like I say, I’d bet it’s a serial version of the same UDP command set. It would need to have some format changes to be RS232. May be encoded with that “bit swizzling”, or maybe not.

Or I could be wrong and it may look nothing like the UDP and may be limited to only what the BWK301R remote already does.

This sound better than adding a competing source of UDP data. I can’t predict what would happen if LB is telling it to do one thing and a foreign UDP source is telling it to do something else. Or, if the remote asks for a read-back of the current coords and LB sees this unsolicited UDP data being broadcast out, what happens?

Actually, the IDEAL solution is right here- I can say for sure the LinuxCNC wireless MPG “XHC-HB04” is actually linked via a Hantec chip as a wireless USB keyboard. That MPG has a USB dongle receiver and it’s just implemented as a wireless kb to the OS. All it’s sending is keystrokes outside the normally defined range. All that needs to happen is you plug its dongle into the PC, LB must look for these keystrokes and use them, and bingo, the Ruida- or ANY device LB is controlling- has a cheap, sophisticated remote solution off the shelf.

Unfortunately, LB isn’t open source so we can’t do this work, and that’s not a product LB can own unless they want to sell it as an add-on licensed sw feature.

1 Like

I’m actually using the <myfile.h> format but when posting the code here it made the characters between “<” and “>” disappear :smile: so I just deleted the first “<”. Don’t know the right escape character…

1 Like

Interesting, I was wondering about the antenna and port and how the real controller communicates.

I don’t know how to sniff a serial communication but I think it would be easier to log the communication between the ACS App and the DSP since we know the protocol/commands. I would appreciate any suggestions on tools that can log such communication.

I fired up Wireshark and threw some bytes at UDP 50207 using python. When I send 0xCC, I get a 0xCC ACK back plus a second packet (eg: ‘a5 68 00 00 3d 04 6d 00 00 24 4f 6f’). Sending 0xCE (which the wiki says “is sent regularly as a sort of keep-alive packet”) gets a 0xCE back. Sending any of the “a5 commands”, e.g. 0xA55001 (-Z Down), yields no response.

I wish I had a copy of the apparently gone away Android app I could sniff to see what it did.

I will try Wireshark myself, sounds interesting.

What app do you mean? The ACS app is still available.

Happen to have a link? I couldn’t find a live one.

【Ruida Technology】lasercut remote control app(apple).rar
Download address: Download of Motion Control Software and V8 software-Ruida Technology

【Ruida Technology】lasercut remote control app(android).rar
Download address: https://www.rdacs.com/en/download?id=58&m=software&a=lasercut%20remote%20control%20app&v=98

Success!

Thanks for the link to the Android app. I connected to my network via a wireguard tunnel and used tcpdump to capture the traffic between the app and the controller. Long story short:

  • 0xCC is basically a “connect” handshake. Send it first.
  • 0xCD is apparently a “disconnect” packet the app sends when it disconnects. (I don’t know whether it has actual effects.)
  • 0xCE is apparently also sent sometimes, possibly useful as a keepalive packet.
  • You must send the UDP packets to the controller’s port 50207 from source port 40207 – in python, you bind the sending socket to the port (and IP, if desired) after you create the socket.
  • You can actually ignore the responses entirely and still send commands, but they do have data. (The 0xA568… packets have coordinates of some kind. I made a quick controller impersonator and connected the app to it, and I could change the coords the app displays, although I didn’t bother trying to figure out the encoding and data.)

Here’s some quick and dirty python that successfully “connected” and sent some test key signals:

import socket
from time import sleep

#Source IP and port for sending UDP packets.
ip = '192.168.105.222'
port = 40207

#Ruida IP and port for receiving UDP packets.
co2 = '192.168.105.100'
cport = 50207

#Create the UDP socket...
out = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#...then bind it so the source of the UDP packets is port 40207.
out.bind((ip,port))

#Send a UDP packet.
def sendUDP(hexes):
    out.sendto(bytearray.fromhex(hexes),(co2,cport))

def test():
    #First, send the handshake to connect.
    sendUDP('cc')
    #Now send five half-second -X presses.
    for _ in range(5):
        sendUDP('a55001') #-X keydown
        sleep(0.5)
        sendUDP('a55101') #-X keyup
        sleep(0.5)

test() #Yay! It works!

1 Like

Great work!

I will try to implement this in C and get it to work on the Esp32.

Have you tried connecting directly to the ruida and seen some movement?

I did indeed. The python above was a copy and paste of me successfully moving the laser head from a python console.

(Well, I deleted the typo lines, added comments, and renamed everything to something better than the single letters I was typing in a live console – it may be quick and dirty, but I have to have some standards. :sweat_smile:)

It works!

I got it working and it’s so exciting. The missing piece was the handshake 0xCC and setting up the source port with udp.begin(40207)

I’ll keep you updated when I’ve got all the functions implemented and tested.

Thanks for the great help!

1 Like

So, apparently the “Trace On/Off” is to toggle the signal to the LFS (Live Focus System) which is available for Ruida-controlled laser cutters (the high-power metal-cutting machines), and “Laser Gate” appears as if it may also be related to such machines (for enabling/disabling the laser). Seems like ignoring those two commands is logical.

As for the rest of the non-axis-movement commands, the all seem pretty straightforward, as they work basically like they would on the control panel on the machine. (“Pulse” has a brief delay before firing, vs. being instant at the button press on the machine.)

The one exception is “Speed”, which doesn’t have its own button on the machine. Took just a bit of looking into to figure out what the “Speed” command does. If it’s enabled in the controller, it toggles between whatever “fast” and “slow” movement speeds you’ve configured in the machine settings.

  1. Open Machine Settings.
  2. Go down to the “Miscellaneous” section.
  3. Toggle “Enable wireless panel speed shift” on/true.
  4. Set the “Wireless panel speed fast (mm/sec)” to whatever you wish.
  5. Set the “Wireless panel speed slow (mm/sec)” as well.

For those who aren’t familiar with the app, a quick tap of the movement buttons moves in tiny steps. Holding a movement button down shifts to continuous movement. If the speed shift toggle is enabled, you can set two different movement speeds. (The speed setting is relevant to continuous movement and to the brief taps. I set 200mm/s and 10mm/s, and at slow speed, I can get three or four taps per tenth of a millimeter on the display.)