I've been hacking a bit on some Logix stuff, just wanted to share something.
- serve up an L5X file as Logix device you can connect to
kevin@Kevins-MacBook-Pro ~/Desktop> docker run --rm \
--name logix-l5x-server \
--network logix-test \
--mount type=bind,source="$PWD/Example.L5X",target=/data/controller.L5X,readonly \
logix-l5x-server:latest \
/data/controller.L5X
Logix server loaded /data/controller.L5X
TCP endpoint: /[0:0:0:0:0:0:0:0]:44818
UDP discovery: /[0:0:0:0:0:0:0:0]:44818
- or programmatically define the Logix tags you want to serve up
private object Types {
data class Motor(val speed: Float, val temperature: Float)
val motor = logixStruct("Motor") {
val speed: LogixMemberRef<Float> = real("Speed", Motor::speed)
val temperature: LogixMemberRef<Float> = real("Temperature", Motor::temperature)
decode { Motor(this[speed], this[temperature]) }
}
}
suspend fun main() {
val heartbeat = MutableStateFlow(0)
val motor = MutableStateFlow(Types.Motor(speed = 1_200f, temperature = 42.5f))
val model = logixModel {
tags {
for (i in 1..3) {
dint("DInt$i", initial = i)
}
dint("Heartbeat")
tag("Motor1", Types.motor, initial = motor.value)
}
}
val server = logixServer(model) {
bindings {
bind("Heartbeat", LogixTypes.DINT, heartbeat.asLogixValue())
bind("Motor1", Types.motor, motor.asLogixValue())
}
}
server.start().getOrThrow().also {
println("Logix server listening on ${it.tcpAddress}; browse Heartbeat and Motor1")
}
try {
coroutineScope {
launch {
while (currentCoroutineContext().isActive) {
delay(1.seconds)
heartbeat.update { it + 1 }
motor.update { it.copy(speed = (it.speed + 25f) % 1_800f) }
}
}
awaitCancellation()
}
} finally {
server.stop().getOrThrow()
}
}
works with IA Logix driver and @pturmel's EtherNet/IP module.
L5X support is a little janky. Predefined/builtin Rockwell types don't work yet.
If you look into my EtherNet/IP driver jars you can find definitions for all of the predefined types up to V35 as resources.
Package com.automation_pros.enip1.driver.init.
I'll take a look to see what you did. Did you go firmware-by-firmware version up to 35 to see if they changed in between or anything like that?
Yes, extract and diff. And there are differences between L7x and L8x series.
There's a checkbox in my Generic Client type to exhaustively probe all 4095 possible structure IDs, whether used or not. So they end up in the diagnostic exports. Post-processed for convenience in the Host Device type.
That sounds like a painfully monotonous process 
Changing the firmware in my lab hardware is the monotonous part. Then tweaking the target Studio 5000 to make sure there are tags of each type to help distinguish public type names (Studio 5k and L5X) from internal type names (as discovered in controller browse).
Once probed to get the driver diagnostics, it's easy.
yeah that's the part I mean.
The main impetus here is so that I can do something like this:
val server = logixServer {
interfaceIdentity =
EnipServerIdentity(
serialNumber = 0x4E_534C_54u,
productName = "$CONTROLLER_COUNT-slot test chassis",
)
repeat(CONTROLLER_COUNT) { slot ->
controller(slot = slot, model = testModel) {
identity =
LogixEndpoint.DEFAULT_IDENTITY.copy(
serialNumber = (slot + 1).toUInt(),
productName = "test controller slot $slot",
)
}
}
}
to spin up 256 or whatever instances of something I can connect to, then maybe containerize it and multiply it further, to test the scalability of the current (and a future) Logix driver.
You can already automate creation of 100 slots with my Host Device (with direct manipulation of resources in v8.3, or the new system.config functions) in an Ignition gateway. Each slot gets its own serial number automatically. I'd be happy let you internally use dev keys for my module for this sort of thing.
Thanks for the offer... not necessary any more, that code above isn't theoretical 
This would be an amazing tool to have even if I can't write to tags just for testing (although we have a couple licenses of Logix Echo now to test things with). v38 on the L9x processors though have gotten me all frustrated since I can't stand PlantPAx and am forced to use the P_PID blocks on the new processors since they got rid of the PIDE blocks on those newer processors.
Definitely one thing that matters the most that I think a lot of users run into is the lack of optimization of UDTs/AOIs. I've ran lots of tests on our own standardized AOIs to try to strike a balance between performance and usability (avoiding arrays and putting both a "Data" UDT for faster data and a "Meta" UDT for rarely changing strings like descriptions and engineering units in our AOIs so we're not creating multiple tags per AOI instance). We're getting 60-70k tags per PLC instance running just fine with various scan groups based on usage. Meta is 60s direct, live values are 1s direct, and setpoints, configurations vary around 2-5s direct with some being 60s/2s leased. If I could get everyone to get Phil's driver, I'd switch it all over to 1s direct.
Edit: (Except for the new PlantPAx P_PID, which we're going to experiment with on this first project using them to see how much it kills performance.) And another thing with those blocks....they literally have internal tags that are only accessible externally but not visible inside the PLC (all the internal operator tags..OCmd, OSet, etc) which in my opinion is horrible practice due to a progammer looking at logic should be able to see everything.
I'll be doing the AOI reverse engineering at some point. I've laid out / planned the "experiment", just don't have time to actually do it, and it doesn't make sense until plans firm up to actually do the new driver.
No amount of reverse engineering or alternative drivers will help you if you're stuck with AOIs you can't edit to remove the tags that aren't externally accessible, though.
Or worse, built-in instructions (not just new PlantPax) with similarly inaccessible members. 
I solved this sorta here: Ignition & Logix Best Practice Tips for better communications - #83 by pturmel
Basically we have a UDT in Ignition that represents the data we want to use. The in the PLC we have a Wrapper AOI that wraps the internal AOI/Instruction/ETC... and maps the data to a PLC UDT that matches the Ignition UDT. Anything that needs to write the PLC side is done via the divert write tag actors in @pturmel toolkit.