Logix Tag Simulator

I've been hacking a bit on some Logix stuff, just wanted to share something.

  1. 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
  1. 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 :grimacing:

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.