Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# pyOLCB
An easy to use python implementation of OpenLCB (LCC) protocols, designed to interface with CAN (and TCP/IP via gridconnect and the native OpenLCB specification in a future release).
An easy to use python implementation of OpenLCB (LCC) protocols, designed to interface with CAN and TCP/IP via GridConnect format.

This is very much a **work in progress**, please don't expect it to function fully for a while.

Documentation is available at [https://www.uncommonmodels.com/pyOLCB](https://www.uncommonmodels.com/pyOLCB)

## CAN Example

```python
from pyolcb import Node, Address, Event, Interface
Expand All @@ -15,5 +16,23 @@ interface = Interface(can.Bus(interface='socketcan', channel='vcan0', bitrate=12

node = Node(address, interface)

node.produce(Event(125))
```

## TCP/IP Example

```python
from pyolcb import Node, Address, Event, Interface
import socket

# Connect to an OpenLCB hub via TCP/IP
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 12021)) # Connect to GridConnect hub

address = Address('05.01.01.01.8C.00')
interface = Interface(sock)

node = Node(address, interface)

node.produce(Event(125))
```
241 changes: 241 additions & 0 deletions examples/tcp_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""
Example: OpenLCB Node over TCP/IP using GridConnect format

This example demonstrates how to create an OpenLCB node that communicates
over TCP/IP using the GridConnect ASCII protocol. This is useful for
connecting to OpenLCB hubs, simulators, or other TCP-based tools.

To run this example, you'll need:
1. An OpenLCB hub or simulator listening on TCP port 12021
2. Or you can test with two instances of this script (one as server, one as client)
"""

import socket
import pyolcb
import time
import threading


def simple_tcp_client_example():
"""
Simple example of connecting to an OpenLCB hub over TCP/IP
"""
print("=== Simple TCP Client Example ===")

# Connect to an OpenLCB hub
# Replace 'localhost' and 12021 with your hub's address and port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect(('localhost', 12021))
print("Connected to OpenLCB hub at localhost:12021")
except ConnectionRefusedError:
print("Could not connect to hub at localhost:12021")
print("Make sure an OpenLCB hub is running on that port")
sock.close()
return

# Create an interface
interface = pyolcb.Interface(sock)

# Create a node with a unique address
address = pyolcb.Address('05.01.01.01.8C.00')
node = pyolcb.Node(address, interface)

print(f"Created node with address: {address}")

# Produce an event
print("Producing event 125...")
node.produce(pyolcb.Event(125, address))

# Give some time for the message to be sent
time.sleep(0.5)

# Clean up
sock.close()
print("Disconnected from hub")


def tcp_listener_example():
"""
Example of setting up a listener for incoming messages
"""
print("\n=== TCP Listener Example ===")

def message_handler(message):
"""Called when a message is received"""
print(f"Received message: MTI={hex(message.message_type.value)}, "
f"Source={message.source.get_alias() if message.source else 'unknown'}")
if message.data:
print(f" Data: {message.data.hex()}")

# Connect to hub
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect(('localhost', 12021))
print("Connected to OpenLCB hub at localhost:12021")
except ConnectionRefusedError:
print("Could not connect to hub at localhost:12021")
sock.close()
return

# Create interface
interface = pyolcb.Interface(sock)

# Register the listener
interface.register_listener(message_handler)

# Create a node
address = pyolcb.Address('05.01.01.01.8C.01')
node = pyolcb.Node(address, interface)

print(f"Created node with address: {address}")
print("Listening for messages for 5 seconds...")

# Listen for a while
time.sleep(5)

# Clean up
interface.stop_listener()
sock.close()
print("Stopped listening and disconnected")


def peer_to_peer_example():
"""
Example of two nodes communicating directly over TCP
"""
print("\n=== Peer-to-Peer Example ===")

received_events = []
event_received = threading.Event()

def event_consumer(message):
"""Called when an event is received"""
print(f"Node 2 received event!")
received_events.append(message)
event_received.set()

# Create a simple server socket
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind(('127.0.0.1', 12022))
server_sock.listen(1)

print("Server listening on 127.0.0.1:12022")

# Accept connection in a thread
client_sock = None

def accept_connection():
nonlocal client_sock
client_sock, addr = server_sock.accept()
print(f"Accepted connection from {addr}")

accept_thread = threading.Thread(target=accept_connection)
accept_thread.start()

# Connect from client
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('127.0.0.1', 12022))
print("Client connected to server")

# Wait for accept to complete
accept_thread.join(timeout=2)

# Create interfaces
client_interface = pyolcb.Interface(client_socket)
server_interface = pyolcb.Interface(client_sock)

# Create nodes
node1_address = pyolcb.Address('05.01.01.01.8C.10')
node1 = pyolcb.Node(node1_address, client_interface)
print(f"Node 1 created with address: {node1_address}")

node2_address = pyolcb.Address('05.01.01.01.8C.20')
node2 = pyolcb.Node(node2_address, server_interface)
print(f"Node 2 created with address: {node2_address}")

# Register event consumer on node 2
event = pyolcb.Event(100, node1_address)
node2.add_consumer(event, event_consumer)
print(f"Node 2 registered consumer for event {event.id.hex()}")

# Produce event from node 1
print("Node 1 producing event 100...")
node1.produce(100)

# Wait for event to be received
if event_received.wait(timeout=2.0):
print("Event successfully received by Node 2!")
else:
print("Event not received within timeout")

# Clean up
server_interface.stop_listener()
client_interface.stop_listener()
client_socket.close()
client_sock.close()
server_sock.close()
print("Peer-to-peer communication complete")


def gridconnect_format_example():
"""
Example showing the GridConnect ASCII format
"""
print("\n=== GridConnect Format Example ===")

# Create a message
address = pyolcb.Address('05.01.01.01.8C.00')
address.set_alias(0x123)

message = pyolcb.Message(
pyolcb.message_types.Initialization_Complete,
pyolcb.utilities.process_bytes(6, '05.01.01.01.8C.00'),
address
)

# Convert to GridConnect format
gridconnect = message.to_gridconnect()
print(f"Message in GridConnect format: {gridconnect}")

# Parse it back
parsed = pyolcb.Message.from_gridconnect(gridconnect)
print(f"Parsed message type: {hex(parsed.message_type.value)}")
print(f"Parsed source alias: {hex(parsed.source.get_alias())}")
print(f"Parsed data: {parsed.data.hex()}")

# Show the format breakdown
print("\nGridConnect format breakdown:")
print(" : - Start delimiter")
print(" X - Extended frame")
print(" 19100123 - CAN arbitration ID (hex)")
print(" N - Data separator")
print(" 050101018C00 - Message data (hex)")
print(" ; - End delimiter")


if __name__ == '__main__':
print("OpenLCB over TCP/IP Examples\n")

# Show GridConnect format
gridconnect_format_example()

# Try peer-to-peer (always works)
peer_to_peer_example()

# These require an external hub
print("\n" + "="*60)
print("The following examples require an OpenLCB hub at localhost:12021")
print("You can skip them if you don't have a hub running")
print("="*60)

response = input("\nRun hub-dependent examples? (y/n): ")
if response.lower() == 'y':
simple_tcp_client_example()
tcp_listener_example()
else:
print("Skipping hub-dependent examples")

print("\nExamples complete!")
83 changes: 75 additions & 8 deletions pyolcb/interface.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,105 @@
import can
import socket
import asyncio
import threading
from .message import Message
from .address import Address
from enum import Enum


class InterfaceType(Enum):
CAN = 0
TCP = 1


class Interface:
network = []
phy = None
connection = None
_tcp_listener_thread = None
_tcp_running = False
_tcp_buffer = ""

def __init__(self, connection: can.BusABC | socket.socket) -> None:
if isinstance(connection, can.BusABC):
self.connection = connection
self.phy = InterfaceType.CAN
elif isinstance(connection, socket.socket):
self.connection = connection
self.phy = InterfaceType.TCP
self._tcp_buffer = ""
else:
raise NotImplementedError("TCP/IP support is not yet implemented")
def send(self, message:Message):
raise TypeError("Connection must be either can.BusABC or socket.socket")

def send(self, message: Message):
if self.phy == InterfaceType.CAN:
can_message = can.Message(arbitration_id=message.get_can_header(), data=message.data, is_extended_id=True)
return self.connection.send(can_message)

def register_connected_device(self, address:Address):
if not address in self.network:
elif self.phy == InterfaceType.TCP:
gridconnect_frame = message.to_gridconnect()
return self.connection.sendall(gridconnect_frame.encode('ascii'))

def register_connected_device(self, address: Address):
if address not in self.network:
self.network.append(address)
return self.network

def register_listener(self, function:callable):
def register_listener(self, function: callable):
if self.phy == InterfaceType.CAN:
can.Notifier(self.connection, [function])
elif self.phy == InterfaceType.TCP:
# Start TCP listener thread
if self._tcp_listener_thread is None or not self._tcp_listener_thread.is_alive():
self._tcp_running = True
self._tcp_listener_thread = threading.Thread(
target=self._tcp_listener_loop,
args=(function,),
daemon=True
)
self._tcp_listener_thread.start()

def _tcp_listener_loop(self, callback: callable):
"""
Internal method to listen for TCP messages and call the callback function.
"""
while self._tcp_running:
try:
# Receive data from socket
data = self.connection.recv(4096)
if not data:
# Connection closed
break

# Add to buffer and decode
self._tcp_buffer += data.decode('ascii', errors='replace')

# Process complete frames (ending with ';')
while ';' in self._tcp_buffer:
frame_end = self._tcp_buffer.index(';')
frame = self._tcp_buffer[:frame_end+1]
self._tcp_buffer = self._tcp_buffer[frame_end+1:]

# Parse and callback
message = Message.from_gridconnect(frame)
if message is not None:
callback(message)

except socket.timeout:
continue
except (ConnectionError, OSError):
# Connection errors - stop listening
break
except Exception:
# Other errors - keep listening but continue
continue

def stop_listener(self):
"""
Stop the TCP listener thread if running.
"""
if self.phy == InterfaceType.TCP and self._tcp_running:
self._tcp_running = False
if self._tcp_listener_thread is not None:
self._tcp_listener_thread.join(timeout=1.0)

def list_connected_devices(self):
return self.network
Loading