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
- 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.
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:
Post a Comment