 Hi. First of all, I wanted to really say thank you for inviting me to your cool conference here. I know, being from the Apache Software Foundation, I'm not actually part of the Linux Foundation, but I think both of our foundations can profit a lot from each other, and I really hope that this talk will help sort of build some bridges for future core elaborations. So let us get started with this talk. First of all, who am I? Well, my name is Christopher Dutz. I'm a senior software engineer at a company called MAPT. There I mainly work on the direct communication with building automation systems. But besides that, I'm a total open source enthusiast, so most of my time goes to all sorts of different open source projects. This has made me a committer in really a lot of Apache projects and also a member of the Apache Software Foundation. What honors me most is that currently I'm serving as vice president of the Apache PLC4X top level project. If you want to follow me, sort of, this is where I post short versions of what I achieved and, well, also a little fun stuff. Follow me on Twitter. LinkedIn, that's where I usually, it's sort of the joke free part where I usually just describe the technical facts behind what I'm working on. Yeah, so let's get started. What are we going to be talking about today? First of all, I won't be expecting that all of you know what Apache PLC4X is, so I'll just give a little introduction to what it is. I'll show you a little example of some communication workflows and show you what challenges come up with a project like PLC4X in this sector. I'll show you what we came up with to solve these problems. We name it NSpec. I'll show you what this little system produces as output. It's sort of a code generation framework and so I'll show you what it generates. Also, I'll give you a little very brief introduction into our test frameworks because, well, generating code is one thing and testing stuff is something completely different. We have plans for generating even more parts of our code, so I'll give you a little explanation on what we're planning on adding in the future. Then comes the part that I'm really happy about because I would really love to see some more collaboration between our communities. I went through the Linux Foundation projects and tried to identify places where I think working together really would make sense. Let's get started with PLC4X. This is the project statement. At Apache, every project sort of has this one statement that identifies it. Let me just jump over here. That identifies it. In case of PLC4X, PLC4X is a set of libraries for communicating with industrial programmable logic controllers using a variety of protocols but with a shared API. I'd like to lay emphasis on this last part because, but with a shared API is what makes PLC4X really unique. Right now, as you can see in the background image, we have loads. It's sort of like this in the automation industry. We have hundreds of different protocols. They all share different or they all have different workflows or ways things are done. Integrating all of this into sort of a shared API is actually something quite difficult. Let me just go over here a bit. Our project website is at PLC4X.Apache.org. The goal of the project, as I mentioned, is writing software for any type of PLC. The cool thing is when changing the PLC, only the configuration needs to be adjusted. If anybody of you remembers how it was writing applications for Java applications that communicate with databases before JDBC, well, you really had to sort of tear apart the application if you switch from MySQL to Oracle or back or whatever. You always needed to change your program. With JDBC, they introduced sort of the concept of a connection string and the queries were in string form too. Let's say to keep it simple, that's sort of what we're trying to do with PLC4X. Think of it as a JDBC for industrial automation. We have a strong growing number of supported protocols. Starting last two years, we also had a strong growing number of programming languages that we support. We also sometimes even support features with the project, which protocols actually don't support. Just as a little example, Modbus, the Modbus protocol generally only allows you to read Boolean values, the coils, or short values, the registers. A lot of companies sort of build on top of that to allow sending a real value, a floating point value based on four bytes. It automatically joins together to registers and then sends a real value into registers. PLC4X supports that. We also support, for example, subscriptions on protocols that actually don't support subscriptions by doing all of this subscription stuff in the background. Another thing that PLC4X is really strong with is the strong growing number of integration modules. While it was sort of normal in the early 2000s, that was a really strong number, that you took a driver or a tool and you built the integration for the thing that you wanted. That's sort of not how things work today. Everybody sort of expects to have integration modules to make it really simple to integrate. And PLC4X comes with a really strong growing number of available integration modules. For example, Apache Kafka, Apache NIFI, for Edge and for Camel, we have all sorts of different integration modules. And well, let's say I'd really be happy to see some Linux foundation project integration modules there in the near future. So PLC4X supports these different types of operations. Well, in general, just reading stuff, writing stuff. Well, that's the basic stuff. Well, we have Cyclic, so it says, like, please give me this value every 100 milliseconds without having to ask. Or please give me this value whenever it changes. Or in PLC world, we also have events and alarms. So let's say if somebody hits the big fat off button of the machine, you don't want to pull that. That has to be an event. Well, our subscription API supports that. What we're currently working hard on, and that's why we have these little red hammers behind them, is discovery and browse. Discovery is sort of, you can think of, you have this little device and you stick it into your network and you hit the discovery button and it just finds out every PLC that it is able to detect. And as a result just gives you the connection information that you need to connect to that device. Browse on the other side is if you've already got a connection and you want to know which resources does this have. Let's say browse is definitely the most complex part. I think I'd say 60% of the size of the Go K-Nex driver is mainly focusing on the browse functionality because all these protocols usually don't even support browse functionality. And we have to be smart with it and try to find out, sort of like, it's sort of automated detective work what we're doing there. But this is definitely, those last two parts are definitely the parts that separate PLC for X from any other solution that I know of. So let's have a look at how a typical PLC for Go project looks like. Well, this is just sort of the Go version of the API. Well, first of all, everything is about the driver manager. So this is what handles all of the connections. So in Go we register driver. So in this case we register the S7 driver. And then we ask the connection manager to please give us a connection. In this case it's going to be an S7 connection on that IP. And up here you can register many drivers. In Java this happens automatically. So everything that it finds in the class path is automatically registered. With Go we decided to do this manually because it just reduces the size of the output. So assuming you're building a product that should support, well, 10 or 20 different protocols. Well, you just have 10 or 20 of these registers up here. And then you just continue from here on. So you get connection and as soon as it sees S7 it's going to ask the S7 driver. Get a connection. It's going to wait till it gets a connection. It's going to make sure that the connection is going to be closed at the end. And now once that part that happens on, well, we try to keep the API or the process of reading information sort of the same for every supported operating programming language. So what we do is we get a read request builder and we add queries or fields to that. So in this case we name a field. Well, this is sort of a stupid name field. And we give it an address. And this address we try to keep as in line with industrial standards as possible. So this highlighted section is exactly as you would get it from a Tia portal, the engineering software you use for programming S7 devices. We added at the end a suffix that tells the type because the address itself doesn't really contain type information. So we had to add that because for the S7 device it doesn't really care if it's sort of a real value. It's a four byte signed integer. It's a four byte unsigned integer or even a 46 bit Boolean array. It's sort of like all the same for it. But as we care about the type at the end, we need to sort of convert this information back. So that's why we have the type at the end to make it explicit. So here we're executing the read request and that will return a channel in Go. So this is all sort of blocking code. You can make it a lot simpler but just for explaining things I think this is a lot simpler. So here we're waiting for the read request result. If everything was fine, we're going to check if the response code was okay. If it was, we'll just out get the value from it and output that on the screen. So as you can see, this code would look exactly the same if we were running it against an OPC UA device, a Modbus device. It doesn't really matter. For every type of device PLC4X supports, this would look exactly the same. The only thing that would be different would be the connection strings and the address strings. And to show you what's happening behind the scenes, the red boxes are sort of what the API or what the user does. So you can see, well, he's initially not connected and he wants to connect. So that's when he calls get connection. This is what's happening in the background. When he executes a read request over here, this is what actually happens. So he issues a read request and he gets back a response. Everything in between just happens magically. So this is what happens magically. And as you can see, just as an example for the S7 protocol, during this connection procedure here, some parameters are agreed upon. One of them is the size of the packet. And the second one is how many requests in parallel can the PLC queue up before it's starting to discard them. Usually, if you've got an S7 1200, it's three parallel requests and I think it's 240 bytes per request. So in this case, we're getting a large read request in and that has to be split up into four different read requests. As I said, it can only queue up three at once. So the first three go out immediately. And the fourth one is queued up right until we get the first read response from any of these. And as soon as that happens, well, then we can fire off the fourth request. And as soon as all four come back, well, we join the information back together to the read response and send that to the client. So Modbus, for example, doesn't even support multiple items in a request, but the user doesn't notice that because PLC for X just does all the magic behind the scenes. So what are the challenges with this? Well, initially when I started PLC for X, I think it was five years ago, I wrote all of the drivers in Java. But I knew this was not going to be how it will be till the end because it was always planned to support multiple languages. And that's why we call the project PLC for X and not PLC for J. The difficult part with all of this is usually finding out how things work, how the reverse engineering the protocols. So if you've done that once, actually writing the driver, that's the easy part. But we knew that writing manual handwriting drivers in many languages, well, that's just not going to happen. So cogeneration was planned from the start. And I'm not one of these folks who thinks, well, if I didn't write it myself, it just sucks. So I had a look at a lot of different approaches. I think in the end it was something near 10 different frameworks for serialization and transmitting data that I had a look at. But the stupid thing was all of these approaches sort of concentrated fully on how do I get an object or an object structure from A to B without it being changed. But they don't really lay emphasis on how is the format of the message that goes on the wire. So as an example, I had a look at thrift. That looked very promising. But for example, in order to transmit numeric values, the thrift folks decided to have sort of variable length integers. So if you use the least most significant 7 bits of a byte, that is sort of like a 1 byte big integer. But as soon as the topmost bit is 1, they add another byte to it with the same procedure. If the topmost bit is 1, well, it adds another byte. Problem is, for example, in the S7 protocol, we need to transmit the code ff for OK. So as soon as I added ff, well, it added another byte. So it would have been hacking all over the place. And it was just like this for all other approaches that I had a look at. The only one that was different was Apache Daffodil. That's sort of a completely different type of framework. It's a framework where you describe your data format in an XML structure that is highly inspired by XML schema. So I think the actual syntax is a sub part of XML schema. That was actually where I was able to sort of model the data structures that went on the wire. But the problem was it wasn't generated code. It was pure interpretation. And therefore it was just too slow. And beyond that, well, I thought, well, being XML, well, doing code generation from that shouldn't be that difficult. But the community wasn't too happy with an XML-like or XML schema type of syntax. So we decided to do something differently. And what we did was, well, we created something called Nspec. And it was quite interesting how this came to be because I was just so frustrated after all of these attempts. And none of them really got me far. That one of our, I think Julian said, stop whining about all of this stuff. Just write your own. Just write it down on text. Just write a text file, how you would sort of define what you want to define. And then we'll just implement the parsers and the rest. Well, that's what we did. So it was highly inspired by Daffodil. Well, mainly because that was the one thing that got me the farthest till then. But in contrast, it's a text-based format for describing the message structure. But in addition to that, also provide all the information that you need to generate parsing and serialization. We implemented the parser with Antel R4. And, well, I would be super happy if somebody with really cool Antel R4 skills would sort of pop by and just have a look at that because I think we can improve that quite a bit. And the code is generated currently with free marker. We have generation templates for Java, C, and Go. I also have some C-sharp code generation in a branch. And the project is planning and starting to get started on working on Python code generation. Antel R4 and free marker, those are just options. So this is sort of the reference implementation that I built. You could be free to sort of use whatever code generation or output format you want. Or you could even implement different parsers. Here's a little example. So this is the topmost packet in an S7 communication. It's a TPKT packet. But the interesting thing is, well, this is sort of the type name. And it consists of a few four fields. One of them is a constant field called protocol ID. And it's always expected to be 0x03. After that, the vendor sort of said, well, maybe we need some flags here. So they added a reserved byte. You can see here, we specify the field types. It's an unsigned 8-bit integer. So in this case, we expect this to be just 0 or not used. Now comes something quite interesting because I said we provide all the information needed for parsing and serializing. And this is what happens with the lang field. So it's a 16-bit unsigned integer. But the cool thing is, this is information that can be derived from the data structure itself. You don't have to manually calculate and store that. So the formula with which the system can find out itself is sort of like just have a look at the payload, ask it for its length and bytes, and add four to that. And last but not least, we have the KOTP packet payload. This is a simple field. This means that the simple fields are, you can think of simple properties. If you're thinking in Java, it's just the properties of your projo that's generated by these. A little more complex example because this is something that you see all over the place. So usually you have in these binary protocols, you have some field that tells the parser what type is coming and then it parses things differently. And in this case, you can see we have here, it's sort of like a calculated field, the header length, and then comes something called TPU code. It's an unsigned 8-bit integer. And here we name things discriminator. And the thing is, this is a field that directly decides what's on the type of the object. So there's actually no need to store that because you can sort of get, if you've got an object and you want to serialize that, well, you just ask it for its discriminators and you can output that value. It's sort of like a shortcut for an implicit value. And the interesting thing now is the type switch. So as soon as you've got a discriminated type, you need at least a type switch. And only if you've got a type switch can you use discriminators. So in this case, we're passing the TPU code to the type switch. So this is now compared to these case statements. So if the TPU code is OXF0, well, then we've got a CoTP packet data. If it's OXE0, well, then it's a CoTP packet connection request. And well, depending if it's now F0, well, then it reads one bit as an end of transmission and the remaining seven bits of that byte as a TPU ref. So that's sort of like a very quick introduction into MSpec, but now I just want to show you very briefly what our code generation framework generates from that. So this is a little example that we built for Go. I took the TPKT packet as an example because I just showed you the data structure. You remember the protocol ID that was a constant? Well, we generate all of these as constants and you can see the OX3 here. So here comes the actual structure. And I said it only has one simple property in there called payload. And this is of type CoTP packet. The next interesting thing is every message has this interface that provides length in bytes, length in bits, and you have a serialized method that you can use to serialize that to its binary form. We automatically generate the constructors, let's call them. We have these little utilities to cast. There's sort of a speciality of Go because if you have subtypes, well, casting to that is sometimes a bit tricky, so we just generate this code automatically. You can see, well, the length in bits, it's sort of like, we need that because there are some protocols for which it's important if this is the last element. So we have so-called parser arguments and if you use them, they are automatically added here. So what happens here is based on the MSpec, we just calculate how big this data packet will actually be. So we have this 8-bit unsigned value of const, then we have this 8-bit reserved field. As you can see, we just sum up all of the sizes and return them to the application. Well, the length in bytes, as you can guess, it's sort of a convenience method because sometimes the size in bytes matters, but usually PLT4X uses the length in bits because there are crazy protocols that have sort of like four-bit big messages. This is sometimes a bit annoying, but, well, now we come to the actual parser. As you can see, we have these readBuffers and writeBuffers that allow reading a variable number of bits from an input. As you can see here, we're reading 8 bits into an UNT8 in Go. Reading the protocol idea here, we're comparing it to the value that we expected. Here we have a reserved field, and in contrast to that, as you can see here, if the protocol ID doesn't match, we'll throw an error. But for reserved fields, we never knew in the future some company will start using these fields, and so it would really suck if our drivers would just break. So we decided for reserved fields, if the reserved field doesn't match the expected value, well, we just simply log that, hey, we just encountered that this is sort of different than what we expected, so maybe it's worth investigating what's different. You can see here the implicit field. When reading, we read this like every other field, but the thing is we just store it as a local variable. And I think later down, yeah, in the next lines, you can see that we can then simply reference it. So implicit fields, when parsing, they just sort of initialize local variables, and when serializing, they have the code that we put in the serialization expression. Here we have the simple field payload that sort of just forwards to the Cotip packet parse function of that type. Yeah, serialization is similar just the other way around. So in this case, we just write an unsigned int 8 bits and we write this value. I already noticed that we could actually reference the constant in this case. The reserved field, well, it just outputs the value. The implicit field, as you can see, I think two slides earlier, you can see this payload length and bytes plus four. Well, this is what we have here. Where are we implicit field? Yeah, here we are. Payload, length and bytes plus four. In Go, we have to do this insane casting because if you don't cast the right way on every step, you really get into trouble. So I just created the code generator to just cast to everything on every level. Here you can see the payload has a serialized method. We just pass the right buffer and it sends things out. Yeah, so that much for generating Go code. For C, I'll just jump through this because I'm already learning a little late. But as you can see, for C, we're generating a header file that has the data structure. And then it has what we had in Go, the interface, the parse, the serialize, the get length and bytes, the get length and bits. We have all of that in C2. And the implementation comes in the C file where we have the constants again. We have the parse method. It looks similar, but yeah, well as C is different than Go. Well, here we allocate some memory. Here we do a lot of pointer passing around. The methods I have been told are insanely long, but we decided to simply stick with that because that's actually code no user would ever encounter. And it's only used inside the drivers. And we decided to rather take this option then to sort of shortening things and then have collisions. So this way we're just on the safe side. But I think if we're just scrolling through this, all of this code is generated automatically and it works really, really nicely. So now we have code generated for our serializers, our parsers, our models. But testing them is something different because if we're sort of handwriting these drivers, well, we would be generating most of the drivers, but we would be hand implementing most of the tests. So I also knew that we need sort of a portable unit test framework. So that's what I built with this little thing. You specify messages in an XML format and it contains sort of the binary input, which is then passed to the parser. It parses the input and produces a model, which we then serialize to an XML form and compare that to the reference one in the test. And only if this matches, then we will take the parsed object again and serialize it into its binary form and compare it to the original one. And with this we have a full round trip to see if our serializers and parsers really match up. This is a little example. Here we have a Kotp connection request. This is the binary input that we would be seeing on the wire. We tell the test to use a TPKTP packet for parsing this and we're expecting it to produce this output. If that works, well, then it's serialized back again and we check if it matches what we sent in to start with. With this, this works on Java, on Go, on C, it's sort of tricky because, well, let's say, serializing stuff to XML in C without using any external libraries, that is a huge challenge. And let's say the ecosystem for tools that can help you with writing such a thing is almost non-existent. So my plans are to sort of automate this by generating code, sort of like generating the handwritten code, but that's still something I need to do in the future or maybe someone in the community can help with this. And let's say in the PLC4X community, the number of people willing to get their fingers that dirty is sort of a bit limited. But that's not all we have because, well, that just tests the serializers and parsers and, well, let's say drivers as you could see in the beginning where I had this little diagram is a bit more complex. So we also have an integration test framework. And this is similar to the unit test framework, so it's also XML-based, but it uses the complete driver. And we use that to simulate complex interaction. We have set up steps that need to initialize the driver into the correct state. And the unit test then accepts sort of we simulate an API request sent to the driver. This is then processed by the driver. We intercept the IO at the end and sort of consume what the driver produces and we send back in what we want it to have as a response. And in the end, we expect that the driver will respond with an API response and then we just check that. Here's a little example. Wherever I have these three dots, just please excuse me, just imagine a huge block of XML coming here. But what you can see in front of every test that we run, well, we have to sort of, first of all, send a KOTP connection request. Then we have to receive the response to that. Then we have to send a connection request. Then we have to receive the response. Then we have to ask for what type of S7 are you and then we have to interpret the response to that. So as soon as we reach here, the driver is expected to be in the state that we want in order to execute the test. In this case, well, it's just a single element read request. The API request says, yeah, we'll send a read request with these fields. This is sort of like just the alias, well, the German speaking folks here might get the reference. And please use this address. And what happens now is that the driver will produce binary output. If parsed, it should match this structure. So it's compared with that. If that matches, well, it takes this data structure and just serializes that to its binary form and passes that into the driver. I'm just scrolling by all of this. So as soon as we passed in the response, well, then we're expecting the driver to return with an API response. And well, our read request had these fields and we're expecting it to have the field hoods is going to be expected to be a Boolean with a value true. And we're expecting it to have a state of okay. And if this matches, well, then the test is green. And otherwise, well, we'll have in our test report, well, we'll just have a report that a single element read request failed. Yeah. And with this, we're really easy. It's really easy to get quite simple coverage in your drivers over multiple languages. And really looking forward to bringing this coverage to C2. This is mostly manually tested. But we have plans for generating more because we still know there's quite, if you're mass producing drivers, well, there are things that are sort of becoming to get repetitive. So the next thing will be automatically generating the code for request and response interactions. So in all of these protocols based on you generate a request and depending on the values in the request, you expect a response and that's going to be of a certain type. And this is something, especially in C, constructing these requests is sort of annoying. And in other languages like Java and Go, it's not quite as bad, but it's still sort of uncool. And it would be cool if I had some sort of factory where I could say, well, give me a Cotp connection request and it would simply produce a Cotp connection request. I pass in all the arguments that sort of need to be customized in that request. And it also produces everything that I need to handle the response to this request. So all I have to do is pass to the driver. Well, please run this interaction. And that will greatly again simplify things a lot. You can think of in these state diagram that I had in the beginning. This will always be this request and response tuple. But what in the end we really want to do is create a state machine where we can specify the logical structure of such a protocol. And with this we should be able to sort of like probably get up to 98% of the code generated. I already did something like that with Daffodil. So I used SCXML 2.0 in conjunction with Daffodil. And I really managed to write an S7 driver that was able to communicate with a real device just using two XML files as input. Well, unfortunately, it was unbelievably slow. But it was interesting what's possible. Speaking of embedded, when it comes to embedded, well, one thing that matters probably most is size. And all of these different languages have their advantages, advantages and disadvantages. And let's say, let's have a look. Well, we started with Java drivers, but if you just take, if you want to create a little application that does S7 communication well in Java, you can expect some 10 to 15 megabyte big jar thing. But that wasn't the focus of Java. With Java we concentrated on a maximum performance and functionality and comfort for our users. Because usually let's face it, if you've got sort of 46 gigabytes of RAM and multiple terabytes of hard disk, well, the size of the jar really doesn't matter that much. If size really matters. And that was one thing I did in 2020 as part of a EU funded research project. The C drivers, they had sort of the like completely different focus. So I built them with minimum resources in mind. So these C drivers, I think the C driver currently comes something near 100 to 200 kilobytes, not megabytes. And sort of intended for being run on these STM32 size devices. It works with almost no external dependencies. So it's not like we're used to that sort of you have to pull in loads and loads and loads of third party libraries. This works with, I think only the core library and nothing else. And it works on single threaded devices, because when I started this little project, I was focusing on getting it to run on Apache minute. And that is sort of built for not running on non-POSIX systems. So the C drivers, they work great on single threaded devices, but you can also use them on multi-threaded devices. The go drivers are sort of the compromise between all of them. They're getting sort of the most performance on embedded, but let's say I'm running them on sort of 32-bit MIPS systems, 32-bit ARM systems, even 46-bit ARM systems. So everything Raspberry Pi and sort of some edge routers, the go drivers are working nicely on. And I think also if you're spinning up loads and loads and loads and loads of containers like, for example, an edgex foundry, well, I think the go drivers will definitely be the best sort of sweet spot between performance size and features. So speaking of which, I had a look at the Linux Foundation projects because I really would love to see some more interaction between all of our cool foundations. And so what I came up with, well, in the edgex foundry, of course, that was sort of the first that I got in contact with at the OSS summit in Edinburgh, some years ago. I was a little sad when they told me, well, we don't do Java, but yeah, well, now we don't only do Java, so I think the go containers would be a huge asset for the industrial IoT communication part. And as far as I could see on the website, edgex foundry sort of lists up all of the interesting drivers that, for example, PLC4X has to offer in the section commercially available device services through community members. So those are the pay to play parts with PLC4X in there. They would be as free as the rest. Ella Fedge, I had a look at a pretty interesting project, but all I could see was that I think the project has OPC UA and IEC 104, drivers on board. And it says others via a commercial fog lamp project. I had a look at that and all the protocols that I explicitly saw named there was OPC UA and Modbus. So with PLC4X in here, I think this also would be a huge asset for the project. Home edge, after a minute, I didn't quite understand how in the end data was actually going to be received into this home edge system, but a quick full text search told me, well, KNX and BacNet and Modbus, well, that's sort of not a topic at the moment. So I'm expecting that, well, if you want to talk to, if you want to integrate your home automation systems into home edge, well, you will definitely need drivers and well, we have KNX, BacNet, Modbus. We have all of this cool stuff that would enable the project to really directly talk to the systems. Automative grade Linux. Well, starting with version 090, we're shipping our first set of Kanbus drivers. Of course, if somebody tells me he's got a car and he's running automotive grade Linux on it and he told me, yeah, and I'm using your Kanbus driver to sort of like control how much the gas pedal is pressed or, well, I would really get nervous and ask him to get out of his car. But we also have passive mode drivers. So just simply listening to all of the telemetry on the car bus, that might be something that could be interesting for this project. One of the projects that I'm actually using most recently, Yachto, I could imagine that for the Yachto project would be quite interesting to sort of have a layer that you can sort of bring in to automatically have sort of an OPC UA or MQTT bridge. So you simply have to get one of these little embedded devices, build your firmware with the Yachto, add the Pils4X OPC UA server layer and you can talk to your legacy Modbus controllers via OPC UA. I think this might even be the part that I could, better would feel comfortable with whipping up myself, but with all of the others, the problem is usually that getting to learn how a project is set up and how it works, that's the actual hard part. And I have learned in the past that whenever I built an integration for a community that I thought was interesting, only the communities themselves actually know what is needed. Only the communities know the best practices, how something is implemented best. I mean all of the integration modules we have, they have been improved over time, but I know that I wasted a lot of time by simply trying to do something that I didn't understand and I could really use some help. So if any of you folks from any of these projects or even ones I didn't mention are interested in integrating PLC for X into your project, please reach out to us. We're more than willing to help. So let's join forces. Okay, spoiler, I already mentioned most of this. Yeah, so as I mentioned, if you're interested in something like this, just reach out to us. If you want to do that, you can contact me directly on Twitter, LinkedIn, or the Internet usually tells you pretty easily how to reach me, or what would be even better would be to sign up to our mailing list. Maybe even create pull requests, even if I would actually say that most of the integration modules for LF Edge projects or Linux Foundation projects, I would probably rather see on the side of the Linux Foundation because I think it integrates better with your ecosystems. Things that really help the project is talking about PLC for X publicly because the industry sort of is totally paranoid about sharing what they're doing on their shop floors. So we know really a lot of big companies are using PLC for X, but nobody's actively talking about it. And this sort of brings us into this situation where, yeah, well, who uses it? Yeah, lots of people. So who uses it? Either we're not allowed to or they never officially sort of mentioned it. Another thing you could do is follow the Apache Pilsafrex project on Twitter. There are project mascot handles its channel quite nicely. Yeah, so I hope this was interesting for you. And yeah, I really hope on seeing some of you soon in person or on our mailing list. Well, I have to admit having a few beers with you guys would be definitely what I would enjoy most right now. But yeah, again, thanks for having me. And yeah, see you soon.