Using Meerk40t to help create Ruida laser management software?

Did you ever make a functional Ruida emulator? I am part of a makerspace that recently acquired a BOSS LS1630 which uses the Ruida 6445 controller. We are trying to develop a system to restrict access to members who have completed the safety class and to log (and hopefully one day charge) for actual use. I am in the process of writing code to query the controller for run times and other parameters, but I don’t want to tie up the machine by having to run the code against the actual controller. I have been looking for an emulator that would allow me to test my code virtually first.

Yes.

In MeerK40t if you type “ruidacontrol” in console it will work to control the laser while pretending to be a ruida device. If you type “ruidadesign” in console it will take the code you send show it on screen in MeerK40t as it transfers the info over. And “ruidaemulator” will just read out the code you send based on the commands it uses. A number of people are using it to control their M2 Lasers with lightburn.

[22:32:46] Window opened: Console
[22:32:46] window open MeerK40t
[22:32:46] Window opened: MeerK40t
[22:32:58] ruidaemulator
[22:32:58] Ruida Data Server opened on port 50200.
[22:32:58] Ruida Jog Server opened on port 50207.
[22:33:15] ← cc (Checksum match)
[22:33:15] → da00057e (Get 05 7e (mem: 02fe) (Card ID))
[22:33:15] ← da01057e0628014a00 (Respond 05 7e (mem: 02fe) (Card ID) = 1694524672 (0x65006500))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da00057e (Get 05 7e (mem: 02fe) (Card ID))
[22:33:15] ← da01057e0628014a00 (Respond 05 7e (mem: 02fe) (Card ID) = 1694524672 (0x65006500))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da000004 (Get 00 04 (mem: 0004) (IOEnable))
[22:33:15] ← da0100040000000000 (Respond 00 04 (mem: 0004) (IOEnable) = 0 (0x00000000))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da00023d (Get 02 3d (mem: 013d) (User Para 2))
[22:33:15] ← da01023d0000000000 (Respond 02 3d (mem: 013d) (User Para 2) = 0 (0x00000000))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da000026 (Get 00 26 (mem: 0026) (Axis Range 1, Get Frame X))
[22:33:15] ← da0100260000134400 (Respond 00 26 (mem: 0026) (Axis Range 1, Get Frame X) = 320000 (0x0004e200))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da000036 (Get 00 36 (mem: 0036) (Axis Range 2, Get Frame Y))
[22:33:15] ← da01003600000d3660 (Respond 00 36 (mem: 0036) (Axis Range 2, Get Frame Y) = 220000 (0x00035b60))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da000004 (Get 00 04 (mem: 0004) (IOEnable))
[22:33:15] ← da0100040000000000 (Respond 00 04 (mem: 0004) (IOEnable) = 0 (0x00000000))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da00001e (Get 00 1e (mem: 001e) (Auto Type Space))
[22:33:15] ← da01001e0000000000 (Respond 00 1e (mem: 001e) (Auto Type Space) = 0 (0x00000000))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da000544 (Get 05 44 (mem: 02c4) (Read Scan Backlash Flag))
[22:33:15] ← da0105440000000000 (Respond 05 44 (mem: 02c4) (Read Scan Backlash Flag) = 0 (0x00000000))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da00057e (Get 05 7e (mem: 02fe) (Card ID))
[22:33:15] ← da01057e0628014a00 (Respond 05 7e (mem: 02fe) (Card ID) = 1694524672 (0x65006500))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da000b12 (Get 0b 12 (mem: 0592) (Unknown))
[22:33:15] ← da010b120000000000 (Respond 0b 12 (mem: 0592) (Unknown) = 0 (0x00000000))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da00057e (Get 05 7e (mem: 02fe) (Card ID))
[22:33:15] ← da01057e0628014a00 (Respond 05 7e (mem: 02fe) (Card ID) = 1694524672 (0x65006500))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da00057e (Get 05 7e (mem: 02fe) (Card ID))
[22:33:15] ← da01057e0628014a00 (Respond 05 7e (mem: 02fe) (Card ID) = 1694524672 (0x65006500))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da000026 (Get 00 26 (mem: 0026) (Axis Range 1, Get Frame X))
[22:33:15] ← da0100260000134400 (Respond 00 26 (mem: 0026) (Axis Range 1, Get Frame X) = 320000 (0x0004e200))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da000036 (Get 00 36 (mem: 0036) (Axis Range 2, Get Frame Y))
[22:33:15] ← da01003600000d3660 (Respond 00 36 (mem: 0036) (Axis Range 2, Get Frame Y) = 220000 (0x00035b60))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da000004 (Get 00 04 (mem: 0004) (IOEnable))
[22:33:15] ← da0100040000000000 (Respond 00 04 (mem: 0004) (IOEnable) = 0 (0x00000000))
[22:33:15] ← cc (Checksum match)
[22:33:15] → da00001e (Get 00 1e (mem: 001e) (Auto Type Space))
[22:33:15] ← da01001e0000000000 (Respond 00 1e (mem: 001e) (Auto Type Space) = 0 (0x00000000))

It should pose quite convincingly as a ruida device.

3 Likes

There is even a Ruida plugin for Inkscape as I found mention if in the ViziCut issues report.

And now that there is a low-end Ruida controller it’s showing up in more lasercutters. Cool stuff in Meerk40t!

2 Likes

I understand basically what you’re doing there, and I can get the emulator function to appear in the console. But, how are you actually sending the DA00 codes that you show above? i.e what is generating the checksums etc.? So far, I’ve only been able to get it to reply with ‘bad checksum’ no matter what data I’ve provided (whether it’s coming from the same host or another computer (at least the UDP server is active for me :grinning:).

I connect to my “laser” with rdworks or lightburn. The DA00 whatever commands are being sent from RDWorks in that case and my “laser” is telling what RDworks wanted with my laser, and what commands it sent. CC is the reply for things being correct. This is all de-swizzled since Ruida uses a swizzle on all their bytes. So unless you’re sending across the mobile device port it will need swizzling. The checksums are sent according to the ruida protocol and if these are right it replies with command CC.

https://edutechwiki.unige.ch/en/Ruida#Swizzling

I’d guess your issue is that you’re not implementing the protocol yet.

  • The device listens on port 50200 on the laser, and sends from port 40200.

  • The payload is an RD file with the same payload, command, and syntax. It also has the same swizzling so an RD file for a 634XG will be swizzled with 0x11 and not 0x88 for the magic number.

  • UDP packets requires an extra 16 bit checksum for each packet. Since UDP can lose packets it requires an ACK reply after every packet. The checksum doesn’t require any special operations it’s literally just the lower 16 bits sum of the value of each byte in the packet. Beyond those differences the remaining elements are Ruida commands which follow a fairly well understood structure.

  • The first two bytes of any UDP packet are big endian checksum. This response to this is either 0xCC Checksum match or 0xCF checksum fail. On fail the packet must be resent. Too long of a time will cause a time out.

  • In UDP the packet size is limited to 1474 bytes. However most modern versions simply cut at around 1000 bytes. These cuts are permitted to occur mid-command but modernly do not do that. They cut at command boundaries.

  • There is no header, arbitration, or handshaking. Anything that is interpreted as correct will execute.

  • The timeout is about 4 seconds. It expects a reply ACK packet by then.

  • The first chunk will retry other chunks will simply fail.

  • The payload has an EOF command.

These data packets are swizzled as given above. the code used for swizzling depends on the machine a bit, but is usually 0x88 for the magic number.

Ok, now I understand the output you posted.
Right, I don’t have the whole protocol part built out. I’ll need to delve into Jürgen Weigert’s (https://github.com/jnweiger) code a little deeper to see how to incorporate that into a simple query and receive functionality for our system.

My code’s a bit setup to make it easy to understand you need to apply the swizzling.

Oh, thanks. That will help a lot.
Now that I know how you generated the output you posted, I just ran Lightburn and connected it to a psuedo Ruida controller using the Meerk40t emulation. It works! This means, that I also have a testbed to code against.
Thanks again! :clap:

    @staticmethod
    def swizzle_byte(b, magic):
        b ^= (b >> 7) & 0xFF
        b ^= (b << 7) & 0xFF
        b ^= (b >> 7) & 0xFF
        b ^= magic
        b = (b + 1) & 0xFF
        return b

    @staticmethod
    def unswizzle_byte(b, magic):
        b = (b - 1) & 0xFF
        b ^= magic
        b ^= (b >> 7) & 0xFF
        b ^= (b << 7) & 0xFF
        b ^= (b >> 7) & 0xFF
        return b

Is your basic swizzle. Likely with magic set to 0x88.

The checksum is calculated unswizzled and just adding all the bytes up and using the lowest bits of the count.

        data = sent_data[2:1472]
        checksum_check = (sent_data[0] & 0xFF) << 8 | sent_data[1] & 0xFF
        checksum_sum = sum(data) & 0xFFFF
1 Like

If you use ruidacontrol or ruidadesign it’ll interpret the ruida code to control the laser or transfer the shapes or whatever too, which is fun. RDWorks works too.


There’s a couple small todos within the file since I do not know how a real ruida command replies to a couple queries since neither the programs nor the dll files or RDWorks expresses that.

So if you do map out what the correct responses are to 0x05 or 0x54 (Read Run Info), and 0x30 (Upload Info Document x) (usually 0 for file would be fine), and 0x31 that would help me out.

In the DA command (so DA0500…) and DA30… DA31…, there’s supposed to be some reply from the actual machine here but I can’t fake it so lightburn and rdworks say I didn’t respond and disconnected. Mostly later versions of RDWorks send DA05 a lot and it’s annoying. Lightburn requests some document info that I can’t pretend to know the response.

Well, I guess if I get a general Ruida command/query tool put together, maybe we can use it to figure those out.
I’ll let you know how things are progressing.

1 Like

yeah, figured you were just going to query for job times and info stuff from the laser, maybe setup a bouncer so you could specifically gateway access to the device. Which means you could fill in those tiny gaps in my knowledge.

So, after a busy week at the day job, I finally had some more time to devote to this project over the weekend. I have (so far) put together a bunch of utility functions that I can reliably get to give me normal looking responses to calls to the emulator. Now, I just have to try the calls against a REAL Ruida controller. Probably not going to get a chance until Tuesday. Fingers crossed. :crossed_fingers:t3:

4 Likes

Oh, and if you map out what the machine states are could you tell me. I always just reply that the machine is 22 or whatever, but I’ve no clue what that means return: “Machine Status”, 22

But, I’d think other values likely mean things like it’s currently running a job or something. And knowing what numbers those might be could certainly be helpful. I always reply to 0x0200 the same way so the laser never actually relays the state. Consequently software won’t tell the laser to stop doing work because it’s not currently doing any work (even if it is actually doing work).

Sorry if this is long winded and a bit rambling, but …
Well, I did make some more progress.
Once I was finally back on the network where the laser lives, I couldn’t figure out why I wasn’t get ANY responses from it in answer to even simple queries that the emulator gave me no problems with.
I finally remembered that the other source I found talked about the controller listening on port 50200, but replying on 40200. After a 12-hour day at work, I wasn’t thinking really clearly, so it took me more than an hour to remember how to set things up where I had 2 sockets, each one bound to one of the addresses. Then write to the 50200 socket and read from the 40200 socket. I essentially had two parallel python streams running - one with the utility functions cut out of Meerk40t, the other handling the UDP communications - and copied the binary string data from one to the other. It turned out that binding the two sockets to the relevant ports was important. Below is some of the relevant code.

###  a few extra utility functions which are really added to the RuidaEmulator class
###  code because it was easier
def make_checksum(self,data):
	return sum(data)& 0xFFFF

def create_command(self,command_array):
        out = bytearray()
        for it in self.check_bytes(self.make_checksum(self.swizzle(command_array))):
            out.append(it)
        for it in self.swizzle(command_array):
            out.append(it)
        return out
### 
data = [0xDA,0x00,0x05,0x7E]  # Check mainboard and firmware version
ip='192.168.1.109'
out_port=50200
in_port=40200
s_out = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
s_out.bind(('0.0.0.0',out_port))
s_in = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
s_in.bind(('0.0.0.0',in_port))

command = create_command(data)
barray = bytearray(command) # binary data string after swizzling and adding checksum
s_out.sendto(barray,(ip,port))
(reply)6
data, address = s_in.recvfrom(4096)
print (data)
(reply)b'\xc6'  # unswizzles to 0xCC which is our "checksum correct" code
data, address = s_in.recvfrom(4096)
print (data)
(reply)b'\xd4\t\rw\xdb\xcd\xc5K%\xdf\xb1\xa7\xb99\xa7\xbf?\x89'
## unswizzling this gave b'\xda\x01\x05\x7fRDLC-V8.01.67\x00', which, when dropping the
## first 4 bytes of the response code and the terminating null 0x00, says that we have a
## genuine RDLC running version 8.01.67 firmware.

Yay!!!

1 Like

very interesting project! I’m also would like to query my Ruida Controller to get some data. Could you please explain what’s the implementation of your “check_bytes” function? I’m new to python and I don’t see the implementation in your code.
Thanks in advance

He’s likely just parsing the data. MeerK40t has the bulk of that stuff done. It’s pretty easy to follow when you get the code that does the sending and receiving of data. I think whatever that thing does for checkbytes isn’t needed. Since you only need the checksum and swizzle.

2 Likes