Beaver Code LLC OpenMRN Gateway.



The design of the Gateway:

The Open Layout Control Bus (aka. OpenLCB, or LCB/LCC) is used to control a variety of model train layout accessories.
These include turnout controllers, signal head/mast controllers, radio frequency id (aka. RFID) controllers, etc. Future planned devices include block occupancy detectors, uncoupler spotters, "derailers", etc. The gateway (aka. GW) is designed to run on "big iron", ie. desktop class POSIX systems. I am using Ubuntu linux, but any POSIX system could work.

The hardware is controlled with SAMD microtrollers (aka. mcu), primarily the Grand Central M4 Express.
For areas that need less power, you can use the Metro M4 Express or the Feather M4 Express.

A series of boards have been developed to implement the above controllers. They are primarily i2c based, but also include SPI and/or bit-banged designs.

Multiple copies of the GW are run, one per LCB node. All GWs typically run on the same POSIX system, but can be spread across multiple systems. Each GW is connected to the OpenLCB hub on the top end, and a single mcu on the bottom end. The Grand Central mcu supports 6 i2c/SPI buses. Any combination of boards can be used on these buses. As an example, the turnout controller handles up to 16 turnouts, so one mcu could handle up to 96 turnouts. The LCB addressing scheme allows up to 256 nodes (simplest assignment), so many thousands of devices are possible.

The railroad itself is a "dead rail" system controlled with WiFi. It is an outdoor G scale setup, eventually extending many hundreds of feet. As this requires a robust WiFi system throughout the layout, WiFi/Ethernet was the obvious choice for LCB connectivity. Connectivity between the GW and the LCB hub is via TCP/IP. Connectivity between the GW and its mcu is via UDP. Either channel can be done with Ethernet or WiFi.

The GW code is implemented with the Open Model Railroad Network (aka. OpenMRN). The OpenMRN hub is used as is (as opposed to the JMRI hub). The GW itself is built with the OpenMRN libraries. These libraries typically are used on lower end mcus, and thus are designed for use on boards without threading, processes, etc. This requires special coding techniques, primarily "state flow", to simulate threads, etc. As I have pthreads, timers, etc. available, I built a "hybrid" system that takes advantage of this fact.

The LCB setup, registration, housekeeping, ..., are all done with the libraries. The job of handling events is left to my GW code. To cleanly divide the 2 halves, I use the standard LCB startup code, then start a pthread to handle the node specific events. This allows me to write that code without dealing with state flow, or worrying about blocking the state flow machine.


The top level code:

int
appl_main(int argc, char* argv[])
{
    printf("\n\n================================================================================================\n");
    parseArgs(argc, argv);
    
    // MUST be done before anything attempts to access the EEProm!
    sprintf(configFilename, "%s/config.%d", path, nodeOffset + 1);

    // Build the database name.
    sprintf(fileName, "%s/db.%d", path, nodeOffset + 1);
    initializeLCC(nodeOffset + 1);

    // Setup the stack.
    NODE_ID += nodeOffset + 1;                  // node 0 is reserved
    openlcb::SimpleCanStack stack(NODE_ID);     // create the CAN stack
    fprintf(stderr, "NODE_ID: 0x%08lx\n", stack.node()->node_id());

    // Create the configuration data.
    stack.create_config_file_if_needed(cfg.seg().internal_config(), openlcb::CANONICAL_VERSION, openlcb::CONFIG_FILE_SIZE);

    // Create a service to move data between the Hub and gw pthread.
    int pipefd[2];
    pipe2(pipefd, O_DIRECT);                    // a pipe that performs I/O in "packet" mode

    // This port pipes all traffic from a (string-typed) hub to a pipe2() write fd.
    EventPort* eventPortPtr = new EventPort(&stack, pipefd[1], NODE_ID);
    eventPortPtr->pipeAllPackets();

    // Create a pthread to parse/convert/move data to/from LCC and hardware.
    if (auto result = startGwThreads(eventPortPtr, pipefd[0], mcuName, mcuPort) != 0) {
        printf("startGwThread()::result: %d\n", result);
        exit(EXIT_FAILURE);
    }

    stack.connect_tcp_gridconnect_hub(lccName, lccPort);

    stack.loop_executor();

    return 0;
}


The EventPort object, which moves events to/from the openMRN hub and the gateway:

#include "openlcb/SimpleStack.hxx"

// This port sends all traffic from a (string-typed) hub to mcu via tcp/udp.
class EventPort : public HubPort
{
public:
    EventPort(openlcb::SimpleCanStack* stack, int fd, uint64_t node) :
        HubPort(stack->service()),
        stack_(stack),
        fd_(fd),
        node_(node)
    {
    }

    Action entry() override
    {
        string s(message()->data()->data(), message()->data()->size());
        write(fd_, s.c_str(), s.length() + 1);		// +1 for the terminating '\0'

        return release_and_exit();
    }

    void sendEvent(uint64_t eventId)
    {
        stack_->send_event(eventId);
    }

    void sendEvent(int event)
    {
        stack_->send_event((node_ << 16) + event);
    }

    void pipeAllPackets(void)
    {
        stack_->gridconnect_hub()->register_port(this);
	stack_->additionalComponents_.emplace_back(this);
    }

private:
    openlcb::SimpleCanStack* stack_;	// the main stack
    int fd_;				// fd of pipe to the gateway pthread
    uint64_t node_;
};


The gateway thread, which does the heavy lifting:

gwThread(void* argContainer)
{
    // Load the passed in arguments.
    gwThreadArgs_t* args = (gwThreadArgs_t*)argContainer;

    fprintf(stderr, "GW: gwThread started: mcu name: %s, port: %d\n", args->mcuName, args->mcuPort);

    // Create a UDP socket.
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    struct hostent* hostEntry;
    if ((hostEntry = gethostbyname(args->mcuName)) == NULL) {
        perror("gethostbyname");
        exit(EXIT_FAILURE);
    }
    mcuAddress.sin_family = AF_INET;
    mcuAddress.sin_addr = *((struct in_addr*)hostEntry->h_addr);
    mcuAddress.sin_port = htons(args->mcuPort);
    bzero(&(mcuAddress.sin_zero), 8);

    // Set socket timeout.
    struct timeval tv = { 10, 0 };
    if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
        perror("Error");
        exit(EXIT_FAILURE);
    }

    // Setup mcu connection.
    controlMode(sockfd);

    struct pollfd fds[POLL_COUNT];
    fds[0].fd = args->fd;
    fds[1].fd = sockfd;
    fds[0].events = POLLIN;
    fds[1].events = POLLIN;

    char buffer[BUFFER_SIZE];

    printf("Gw: entering main loop\n\n");

    while (1) {
        int result = poll(fds, POLL_COUNT, -1);				// -1: wait forever
	switch (result) {
	default:
            if (fds[0].revents & POLLIN) {				// data from LCC
                int count = read(fds[0].fd, buffer, BUFFER_SIZE);
                if ((count) > 0) {
                    processLCCBuffer(args->epPtr, fds[1].fd, buffer);
                }
            }
            if (fds[1].revents & POLLIN) {				// data from mcu
                int count = recvfrom(fds[1].fd, buffer, BUFFER_SIZE, 0,
                                     (struct sockaddr*)(&mcuAddress), &mcuAddressLength);
               cuAddress), &mcuAddressLength);
                if (count == -1) {
                    perror("recvfrom timed out");
                }
                else {
                    buffer[count] = '\0';	// terminate the string
                    processMCUBuffer(args->epPtr, buffer);
                }
            }
            break;

        case 0:
            printf("poll: %d, unexpected timeout!\n", result);
            break;
        case -1:
            printf("poll: %d, error: %d\n", result, errno);
            break;
        }
    }

    return nullptr;
}