Train Location Detection and Display

 After many years of trying to teach myself the "Python" computer language I gave up on the idea of writing a program that would let me display a line drawing of my layout with lights turning on and off as the train passed a series of detectors as I way out of my depth.  As my railway is battery powered (dead rail) using DCC block occupancy detectors were out of the question.

A chance comment by a friend on a train form regarding ChatGPT sparked interest in resurrecting the idea.

This is how I have managed to direct ChatGPT (the bot) to write the python Code which draws a line diagram of my layout and lets me position sensor "lights" on a laptop screen (a Graphic User Interface aka GUI), Signal settings and points directions are planned to be added once the detection function is working correctly.

The first instruction went like this.

The idea is to use a Picaxe microcontroller to take inputs from a number of sensors then output the data via a serial cable to a laptop which is running a Python program that displays the information received as a line drawing of the layout.  I started with the track outside the shed which is a U shape to get the whole thing started.

This is the reply to the first instructions

The Picaxe Part;

Here's a step-by-step approach to accomplish your project:

This code assumes you're using digital sensors (like reed switches or IR sensors) and that each sensor is connected to an input pin on the PICAXE 14M2. It reads the sensor data, packages it into a serial format, and sends it via the AXE027 serial cable to the PC.

1. PICAXE 14M2 Code

 #picaxe 14M2

symbol sensor1 = pinC.0  ; Sensor 1 input
symbol sensor2 = pinC.1  ; Sensor 2 input
symbol sensor3 = pinC.2  ; Sensor 3 input
symbol sensor4 = pinC.3  ; Sensor 4 input
symbol sensor5 = pinC.4  ; Sensor 5 input

main:
    ; Read sensors and send data via serial
    serout C.0, N2400_4, ("S1:", #sensor1, " S2:", #sensor2, " S3:", #sensor3, " S4:", #sensor4, " S5:", #sensor5, 13, 10)

    pause 1000 ; Wait 1 second before next reading
    goto main

This program continuously reads the sensor values connected to pins C.0 to C.4 and sends them in a formatted string via serial at 2400 baud rate. You can expand the number of sensors based on your needs.

  

This is the Python part;

The first python script did not work because there was no active com port on my laptop so I instructed the bot write new a version bypassing com port.

This it did and suggested that I concentrate on getting the GUI working first so picaxe refinement (pin to sensor allocation) can be worked on later.

I instructed the bot to use "Libre Draw" as an example of line drawing and handling..

These are the steps I then followed to get to where I am now were;

  • Working "Add/Delete" buttons for sensors, signals points and lines (icons for want of abetter word)
  • The ability to moved the generated icons using drag and drop
  • The ability to Drag and Drop and Line Resizing was the next refinement so that I could adjust the line length and position (now I could create angles to simulate points.
  • make the sensors,signals and points icons moveable using drag and drop
  • added a save function so that any changes are stored and displayed again if the program is closed
Future steps include;
  • Making the icons selectable so that they can be removed, at the moment when deleting them the last one added is removed. 
  • Make the sensor icon change colour as a train passes
  • Adding a text box to the icons to make identification easier
  • Adding a "hide/show" button for the edit buttons to prevent accidental pressing
  • add a line width

This is the python code which I have running at the moment.
I will continue to tweak it till it works as I want it to.
I also asked the bot to write a program that uses a Raspberry pi instead of a picaxe and a laptop (it has a problem with the keyboard glitching randomly causing issues with running programs) combination to do the input/output and display processing.
It has done but as I do not have a pi to hand but I do have picaxe 14M2's that may be a project for the future.
The advantage of the pi is that it has GPIO ports that sensors could be connected directly to thus removing the need for a serial cable.
 
This is a work in Progress and will change over time, I will attempt to keep it up to date as time allows.
 
   
Note:- To run the Python code an Integrated Development Environment (IDE) will be needed; I use "Thonny" which is a good one for beginners and I found easy to use.

  The Python Code

 import matplotlib
matplotlib.use('TkAgg')  # Force the use of the Tkinter backend for matplotlib
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
import math
import json
import os

# File to store the track layout
SAVE_FILE = 'track_layout.json'

# Simulated data for sensors, points, signals, and lines
sensors = [{'id': 1, 'x': 2, 'y': 4}, {'id': 2, 'x': 4, 'y': 4}, {'id': 3, 'x': 6, 'y': 4}]
points = [{'id': 1, 'x': 3, 'y': 3}]
signals = [{'id': 1, 'x': 5, 'y': 5}]
lines = [{'id': 1, 'start': (1, 1), 'end': (8, 1)}, {'id': 2, 'start': (1, 1), 'end': (1, 7)}]

selected_line = None  # To keep track of the currently selected line
selected_sensor = None  # To keep track of the selected sensor
selected_point = None  # To keep track of the selected point
selected_signal = None  # To keep track of the selected signal
dragging = False  # Flag to indicate if we're dragging an element
resizing = False  # Flag to indicate if we're resizing a line
dragging_line = False  # Flag for dragging an entire line
resize_start_point = False  # Resizing the start point of the line
resize_end_point = False  # Resizing the end point of the line

# Function to update the track display
def update_track():
    ax.clear()  # Clear the previous track drawing

    # Draw lines representing the track segments
    for line in lines:
        if line == selected_line:
            ax.plot([line['start'][0], line['end'][0]], [line['start'][1], line['end'][1]], 'b-', linewidth=3)  # Highlight selected line
        else:
            ax.plot([line['start'][0], line['end'][0]], [line['start'][1], line['end'][1]], 'k-', linewidth=2)

    # Add sensor locations on the track (display as red circles)
    for sensor in sensors:
        ax.plot(sensor['x'], sensor['y'], 'ro', markersize=10)
        ax.text(sensor['x'], sensor['y'], f"S{sensor['id']}", fontsize=12, verticalalignment='bottom')

    # Add points on the track (display as blue squares)
    for point in points:
        ax.plot(point['x'], point['y'], 'bs', markersize=10)
        ax.text(point['x'], point['y'], f"P{point['id']}", fontsize=12, verticalalignment='bottom')

    # Add signals on the track (display as green triangles)
    for signal in signals:
        ax.plot(signal['x'], signal['y'], 'g^', markersize=10)
        ax.text(signal['x'], signal['y'], f"Sig{signal['id']}", fontsize=12, verticalalignment='bottom')

    ax.set_xlim(0, 10)
    ax.set_ylim(0, 8)
    ax.grid(True)
    canvas.draw()  # Redraw the updated plot

# Button functions (add/remove sensors, points, signals, and lines)
def add_sensor():
    new_sensor_id = len(sensors) + 1
    new_sensor = {'id': new_sensor_id, 'x': 2, 'y': 2}  # Default position
    sensors.append(new_sensor)
    update_track()

def delete_sensor():
    if sensors:
        sensors.pop()  # Remove the last sensor
        update_track()

def add_point():
    new_point_id = len(points) + 1
    new_point = {'id': new_point_id, 'x': 3, 'y': 3}  # Default position
    points.append(new_point)
    update_track()

def delete_point():
    if points:
        points.pop()  # Remove the last point
        update_track()

def add_signal():
    new_signal_id = len(signals) + 1
    new_signal = {'id': new_signal_id, 'x': 5, 'y': 5}  # Default position
    signals.append(new_signal)
    update_track()

def delete_signal():
    if signals:
        signals.pop()  # Remove the last signal
        update_track()

def add_line():
    new_line_id = len(lines) + 1
    new_line = {'id': new_line_id, 'start': (2, 2), 'end': (6, 2)}  # Default start and end points
    lines.append(new_line)
    update_track()

def delete_line():
    if lines:
        lines.pop()  # Remove the last line
        update_track()

# Save function to save layout to JSON
def save_layout():
    layout = {
        'sensors': sensors,
        'points': points,
        'signals': signals,
        'lines': lines
    }
    with open(SAVE_FILE, 'w') as f:
        json.dump(layout, f)
    print("Layout saved!")

# Load function to load layout from JSON
def load_layout():
    global sensors, points, signals, lines
    if os.path.exists(SAVE_FILE):
        with open(SAVE_FILE, 'r') as f:
            layout = json.load(f)
            sensors = layout.get('sensors', [])
            points = layout.get('points', [])
            signals = layout.get('signals', [])
            lines = layout.get('lines', [])
        print("Layout loaded!")
    else:
        print("No saved layout found.")

# Function to check if a point (x, y) is near any line
def line_near_point(x, y, line, threshold=0.2):
    """Check if a point (x, y) is near a line."""
    x1, y1 = line['start']
    x2, y2 = line['end']

    if (x2 - x1) == 0:  # Vertical line
        distance = abs(x - x1)
    elif (y2 - y1) == 0:  # Horizontal line
        distance = abs(y - y1)
    else:
        slope = (y2 - y1) / (x2 - x1)
        intercept = y1 - slope * x1
        distance = abs(slope * x - y + intercept) / (slope**2 + 1)**0.5

    return distance < threshold

# Function to check if a point is near a sensor, point, or signal
def point_near_object(x, y, obj, threshold=0.3):
    """Check if a point (x, y) is near a sensor, point, or signal."""
    distance = math.sqrt((x - obj['x']) ** 2 + (y - obj['y']) ** 2)
    return distance < threshold

# Event handlers for mouse actions
def on_click(event):
    global selected_line, selected_sensor, selected_point, selected_signal, dragging, dragging_line, resizing, resize_start_point, resize_end_point
    if event.inaxes:
        # Reset selections
        selected_line = None
        selected_sensor = None
        selected_point = None
        selected_signal = None
        dragging = False
        dragging_line = False
        resizing = False

        # Check if the click is near any sensor, point, or signal
        for sensor in sensors:
            if point_near_object(event.xdata, event.ydata, sensor):
                selected_sensor = sensor
                dragging = True
                update_track()
                return

        for point in points:
            if point_near_object(event.xdata, event.ydata, point):
                selected_point = point
                dragging = True
                update_track()
                return

        for signal in signals:
            if point_near_object(event.xdata, event.ydata, signal):
                selected_signal = signal
                dragging = True
                update_track()
                return

        # Check if the click is near any line
        for line in lines:
            if line_near_point(event.xdata, event.ydata, line):
                selected_line = line
                x1, y1 = line['start']
                x2, y2 = line['end']
                # Check if near the start point of the line
                if math.sqrt((event.xdata - x1)**2 + (event.ydata - y1)**2) < 0.3:
                    resizing = True
                    resize_start_point = True
                    resize_end_point = False
                # Check if near the end point of the line
                elif math.sqrt((event.xdata - x2)**2 + (event.ydata - y2)**2) < 0.3:
                    resizing = True
                    resize_start_point = False
                    resize_end_point = True
                else:
                    # Otherwise, drag the whole line
                    dragging_line = True
                update_track()
                return

def on_motion(event):
    global dragging, selected_line, dragging_line, resizing
    if dragging and selected_sensor:
        selected_sensor['x'] = event.xdata
        selected_sensor['y'] = event.ydata
        update_track()
    elif dragging and selected_point:
        selected_point['x'] = event.xdata
        selected_point['y'] = event.ydata
        update_track()
    elif dragging and selected_signal:
        selected_signal['x'] = event.xdata
        selected_signal['y'] = event.ydata
        update_track()
    elif dragging_line and selected_line:
        # Move the whole line
        x1, y1 = selected_line['start']
        x2, y2 = selected_line['end']
        dx = event.xdata - (x1 + x2) / 2
        dy = event.ydata - (y1 + y2) / 2
        selected_line['start'] = (x1 + dx, y1 + dy)
        selected_line['end'] = (x2 + dx, y2 + dy)
        update_track()
    elif resizing and selected_line:
        if resize_start_point:
            selected_line['start'] = (event.xdata, event.ydata)
        elif resize_end_point:
            selected_line['end'] = (event.xdata, event.ydata)
        update_track()

def on_release(event):
    global dragging, dragging_line, resizing
    dragging = False
    dragging_line = False
    resizing = False

# Set up the GUI window
root = tk.Tk()
root.title("Model Train Track Editor")

# Create a frame for the buttons
button_frame = tk.Frame(root)
button_frame.pack(side=tk.BOTTOM, fill=tk.X)

# Add buttons for adding/removing sensors, points, signals, and lines
add_sensor_button = tk.Button(button_frame, text="Add Sensor", command=add_sensor)
add_sensor_button.pack(side=tk.LEFT)

delete_sensor_button = tk.Button(button_frame, text="Delete Sensor", command=delete_sensor)
delete_sensor_button.pack(side=tk.LEFT)

add_point_button = tk.Button(button_frame, text="Add Point", command=add_point)
add_point_button.pack(side=tk.LEFT)

delete_point_button = tk.Button(button_frame, text="Delete Point", command=delete_point)
delete_point_button.pack(side=tk.LEFT)

add_signal_button = tk.Button(button_frame, text="Add Signal", command=add_signal)
add_signal_button.pack(side=tk.LEFT)

delete_signal_button = tk.Button(button_frame, text="Delete Signal", command=delete_signal)
delete_signal_button.pack(side=tk.LEFT)

add_line_button = tk.Button(button_frame, text="Add Line", command=add_line)
add_line_button.pack(side=tk.LEFT)

delete_line_button = tk.Button(button_frame, text="Delete Line", command=delete_line)
delete_line_button.pack(side=tk.LEFT)

# Add save button
save_button = tk.Button(button_frame, text="Save", command=save_layout)
save_button.pack(side=tk.LEFT)

# Load the layout if it exists
load_layout()

# Set up the plot area
fig, ax = plt.subplots(figsize=(6, 6))
canvas = FigureCanvasTkAgg(fig, master=root)  # Embed plot in Tkinter
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)

# Connect mouse events
canvas.mpl_connect('button_press_event', on_click)
canvas.mpl_connect('motion_notify_event', on_motion)
canvas.mpl_connect('button_release_event', on_release)

# Draw the initial track
update_track()

# Start Tkinter's event loop
root.mainloop()

 

If you wish to experiment with this code go right ahead but if you could let me know (in the comments section below) what improvements that were made I would be greatly appreciative.

  •  

 

No comments: