Free Your Model Train (FYMT) — Python3 und Arduino

Das Konzept von FYMT

Dokumentation zu FYMT

Thema

Will man den Arduino von einem Rechner aus steuern, bietet sich an, dies mittels eines Python-Skripts zu bewerkstelligen. Hier soll Python3 zum Einsatz kommen. Für die Grafik wird tkinter eingesetzt. Dieses Tool ist seit Langem gut dokumentiert und steht unter allen Plattformen zur Verfügung.

Verwendung findet auch die Bibliothek "serial" zur Kommunikation zwischen Rechner und Arduino über USB.

Gesteuert werden sollen zwei LED über Pulsweitenmodulation. Damit können sie gedimmt werden.

Es dabei sollen nicht nur Kommandos an den Arduino übermittelt werden; vielmehr soll dieser auch Meldungen zurückgeben.

Aufbau

Der simple Aufbau erfolgte auf einer Steckplatine (breadboard). Zwei LED werden jeweil über einen Widerstand mit den Pins D9 und D10 verbunden. Die Stromversorgung erfolgt über die USB-Schnittstelle.

Foto der Steckplatine

Der Sketch für den Arduino

Quellcode

/*
 * Copyright 2017 Michael Stehmann <info@rechtsanwalt-stehmann.de>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 * 
 */
 
String VERSION = "0.2.0";
String NAME = "MoreLEDs.ino";
String LASTAUTHOR = "Michael Stehmann";
String DATE = "2017-05-14";

const unsigned int LEDPin1 = 9;
const unsigned int LEDPin2 = 10;
const unsigned int BAUD_RATE = 9600;

unsigned int brightness1 = 100;
unsigned int brightness2 = 100;

void LEDdimmer(int, int);

void setup() {
	Serial.begin(BAUD_RATE);
}

void printinfo() {
	Serial.print(NAME);
	Serial.print(" ");
	Serial.print(VERSION);
	Serial.print(", lastchange by ");
	Serial.print(LASTAUTHOR);
	Serial.print(" on ");
	Serial.println(DATE);
}

String info() {
	if (Serial.available() > 0) {
		String COMMAND = Serial.readString();
		// Serial.println(COMMAND);
		if (COMMAND == "vvv") {
			printinfo();
			return("");
		}
		else {
			return(COMMAND);
		}  
	}
}

void message (int brightness, int LEDnr) {
	Serial.print("LED No. ");
	Serial.print(LEDnr);
	Serial.print(" PWD: ");
	Serial.print(brightness);
}

void loop() {
	if (Serial.available() > 0) {
		String command = "";
		int LEDnr = 0;
		String LEDStr = "";
		String brightstr = "";
		unsigned int brightness = 100;

		command = info();

		LEDStr = command.substring(0,1);
		LEDnr = LEDStr.toInt();
		brightstr = command.substring(1);
		brightness = brightstr.toInt();
		LEDdimmer (LEDnr, brightness);
 	}
}

void LEDdimmer (int LEDnr, int brightness) {
	// Boundaries for brightness
	if (brightness > 255) {
		brightness = 255;
	} else if (brightness < 20) {
		brightness = 0;
	}
	// Choose LED
	if (LEDnr == 1) {
		brightness1 = brightness;
	} else if (LEDnr == 2) {
		brightness2 = brightness;
	}
	message (brightness1, 1);
	analogWrite(LEDPin1, brightness1);
	Serial.print(", ");
	message (brightness2, 2);
	analogWrite(LEDPin2, brightness2);
	Serial.println("");
}

Anmerkungen

Auch dieser Sketch soll über sich Auskunft geben. Dies soll hier allerdings einzeilig geschehen.

Eingangs werden ferner die LED-Pins, die Baudrate und Vorgabewerte für die Helligkeit der LED definiert.

Das eingesetzte Makefile ist sensibel, was die Deklaration einer Funktion vor ihrem Aufruf angeht. Da die Funktion "LEDdimmer" zwar schon in der Funktion "loop" aufgerufen wird, ihr Inhalt im Quelltext aber erst danach erscheint, muss sie zuvor deklariert werden. Dies geschieht mit der Zeile:

void LEDdimmer(int, int);

In der Funktion "info" wird geprüft, ob die Selbstauskunft als Meldung zurückgegeben werden soll oder ein Kommando zum Ein- oder Ausschalten oder zum Dimmen der LED ausgeführt werden soll.

Die Funktion "message" gibt die PWM-Werte beider LED als Meldung zurück.

Die Funktion "LEDdimmer" steuert die Helligkeit der LED. Das Kommando hierzu ist eine Zeichenkette bestehend aus vier Ziffern. Die erste Ziffer (1 oder 2) bestimmt, welche LED geregelt werden soll, die restlichen drei Ziffern (000 bis 255) deren Helligkeit.

Python3 Kommandozeile

Quellcode

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  MoreLEDs.py
#  version: 0.1.1
#  
#  Copyright 2017 Dr. Michael 
#  
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#  
# This program works with the sketch 'MoreLEDs.ino'
#
# For Debian you have to install this additional package
# -- python3-serial

import serial
from time import sleep

endsignal = 0

def connect2arduino():
	locations=['/dev/ttyUSB0','/dev/ttyUSB1','/dev/ttyUSB2','/dev/ttyUSB3',
'/dev/ttyS0','/dev/ttyS1','/dev/ttyS2','/dev/ttyS3']  
  
	for device in locations:  
		try:  
			print ("Trying "+device)
			arduino = serial.Serial(device, 9600)
			print("successfully!") 
			break
		except:  
			print ("Failed to connect on "+device)

	return arduino

def comm2ardino(arduino):
	global endsignal
	commandstr = input("Your command: ")
	# print (commandstr)
	if commandstr == "end":
		endsignal = 1
	else:
		try:  
			arduino.write(commandstr.encode('utf-8'))
			arduinoanswer(arduino)
			if commandstr == "vvv":
				arduino.write("".encode('utf-8'))
				arduinoanswer(arduino)
		except:   
			print("Failed to send!")

def arduinoanswer(arduino):
	sleep(1)
	answer = arduino.readline()
	answer = answer.decode("utf-8", "ignore")
	# This also works, but it is dirty
	# answer = str(arduino.readline())
	# answer = answer.replace("b'","")
	# answer = answer.replace("\\r\\n'","")
	print(answer)

def infotxt():
	print("")
	print ("Please insert 'vvv' for programm information")
	print ("Insert '1200' for LED No. 1 and PWD = 200 or")
	print ("'2050'for LED No. 2 and PWD = 50 (for example)")
	print ("Insert 'end' to leave program")
	print("")

def main():
	global endsignal

	arduino = connect2arduino()
	if arduino:
		infotxt()
		while endsignal == 0:
			comm2ardino(arduino)
	return 0

if __name__ == '__main__':
	main()

Anmerkungen

Dieses Skript arbeitet mit dem zuvor vorgestellten Sketch "MoreLEDs.ino" zusammen, wenn sich dessen Kompilat auf dem Arduino befindet.

Zu importieren sind die Bibliotheken "serial" und "time", aus welcher aber nur "sleep" benötigt wird.

Die Funktion "connect2arduino" erzeugt die Verbindung zwischen Rechner und Arduino und gibt ein entsprechendes Objekt zurück. Dabei iteriert das Programm über eine Liste potentieller Schnittstellen bis zur ersten erfolgreichen Verbindung. Im Misserfolgsfall gibt es eine Fehlermeldung.

Die Funktion "comm2ardino" verarbeitet die eingegebenen Kommandos und gibt sie (sofern nicht "end" eingegeben wurde) über die geschaffene Verbindung an den Arduino weiter. Auch hier erfolgt im Misserfolgsfall eine Fehlermeldung.

Die Funktion "arduinoanswer" verarbeitet die Rückmeldungen des Arduino-Programms.

Die Funktion "infotxt" gibt Hinweise zur Bedienung des Programms aus.

Die Funktion "main" ruft zunächst die Funktion "connect2arduino" auf, um die Verbindung herzustellen. Im Erfolgsfalle ruft sie die Funktion "infotxt" auf, um den Benutzer über die Bedienung des Programms zu informieren und nimmt sodann durch Aufruf der Funktion "comm2ardino" Kommandos an den Arduino entgegen, bis "end" eingegeben wird.

Python3 GUI

Bildschirmfoto

Bildschirmfoto des grafischen Benutzeroberflaeche

Quellcode

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  MoreLEDsGUI.py
#  version: 0.1.1
#  
#  Copyright 2017 Dr. Michael <info@rechtsanwalt-stehmann.de>
#  
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#    
# This program works with the sketch 'MoreLEDs.ino'
#
# For Debian you have to install these additional packages
# -- python3-tk
# -- python3-serial


import serial
from time import sleep

# for the GUI
from tkinter import * 
from tkinter import messagebox
from tkinter import scrolledtext 

class DialogMaker(object):

	def __init__(self):
		"""self.fs contains the font size
		   10 seems to be a good default value"""
		self.fs = 10
		self.darkbgcolor = "#EDF25D"
		self.brightcolor = "#F6F9AC"
		
	"""Creates and destroys dialog"""		
	def makedia(self):		
		"""Creates a Tkinter dialog"""
		self.dia=Tk()
		return self.dia

	def diatitle (self, title):
		"""Setting the title of the dialog"""
		self.dia.title(title)
		"""Stores a short name of the running application"""

	def diaminsize(self, diah=600, diav=250):
		"""Minimum size of the dialog window
		   diah = horizontal minimum size
		   diav = vertical minimum size"""
		self.dia.minsize(diah, diav)

	def geometry(self, geostring="+50+0"):
		"""Places dialog on the desktop,
		   the first number is horizontal, the second vertical"""
		self.dia.geometry(geostring)

	def mainloop(self):
		"""Creates the dialog"""
		self.dia.mainloop()
		
	"""Creates menu"""
	def menu(self):
		# self.fs contains the font size
		self.menu = Menu(self.dia) #, font = "SansSerif, "+str(self.fs))
		self.dia.config(menu=self.menu)

	def menuitems(self, menulabel, itemdict):
		submenu = Menu(self.menu)
		self.menu.add_cascade(label=menulabel, menu=submenu, font = "SansSerif, "+str(self.fs))
		itemlist = list(itemdict.keys())
		itemlist.sort()
		for entry in itemlist:
			valuetup = itemdict[entry]
			itemlabel = valuetup[0]
			if itemlabel == "Separator":
				submenu.add_separator()
			else:
				itemcommand = valuetup[1]
				submenu.add_command(label=itemlabel, command=itemcommand, font = "SansSerif, "+str(self.fs))

	"""Creates frame"""	
	def makeframe(self):
		self.frame = Frame(self.dia, borderwidth=2, bg = self.darkbgcolor)
		self.frame.pack(fill="both",expand=1)
		return self.frame

	"""Destroys the frame"""
	def endframe(self):
		self.frame.destroy()
		
	"""General elements to create Tkinter dialogs"""
		
	def button(self, buttontext, command_, row_, column_):
		"""Creates a button"""
		button = Button(self.frame, text = buttontext, command = command_)
		button.grid(row=row_, column=column_, padx=10)
		return button

	def label(self, ausgabetext, row_, column_, columnspan_=1):
		"""Creates a label field"""
		lb = Label(self.frame, text = ausgabetext, bg=self.brightcolor)
		lb.grid(row=row_, column=column_, columnspan=columnspan_, sticky=E, padx=5, pady=10)
		return lb

	def scrtxtfield(self, row_, column_, columnspan_=1, width_=75, height_=18):
		stf = scrolledtext.ScrolledText(self.frame, width=width_)
		stf.grid(row=row_, column=column_, columnspan=columnspan_)
		stf["height"] = height_
		return stf


class LEDsGui(DialogMaker):

	def dia(self, flag=1):

		"""set some initial values"""
		self.brightness_1 = 100
		self.brightness_2 = 100
		self.stop1 = 0
		self.stop2 = 0

		"""Creates the dialog"""
		self.makedia()
		self.diaminsize()
		self.geometry()
		self.diatitle("Dimming LEDs with Python")

		"""Creates the menu"""
		self.menu()
		tasklabel = "Tasks"
		taskitemsdict = {
			1 : ("Connect to Arduino", self.makeconnection),
			2 : ("Arduino info", self.arduinoinfo),
			3 : ("Close", self.finishprogram)
			}
		self.menuitems(tasklabel, taskitemsdict)

		"""Creates a frame"""
		self.makeframe()

		"""Creates elements"""

		"""First set"""
		self.label("LED 1", 0, 0)
		self.button("<", self.darker_1, 0, 1)

		self.vtxt_1 = StringVar()
		self.b_label_1 = Label(self.frame, textvariable = self.vtxt_1, bg = "white")
		self.b_label_1.grid(row=0, column=2, padx=5, pady=10, sticky=EW)
		self.vtxt_1.set(str(self.brightness_1))

		self.button(">", self.brighter_1, 0, 3)
		self.stopbutton1 = self.button("off", self.stop_1, 1, 2)

		"""Second set"""
		self.label("LED 2", 2, 0)
		self.button("<", self.darker_2, 2, 1)

		self.vtxt_2 = StringVar()
		self.b_label_2 = Label(self.frame, textvariable = self.vtxt_2, bg = "white")
		self.b_label_2.grid(row=2, column=2, padx=5, pady=10, sticky=EW)
		self.vtxt_2.set(str(self.brightness_2))

		self.button(">", self.brighter_2, 2, 3)
		self.stopbutton2 = self.button("off", self.stop_2, 3, 2)

		"""Field for messages"""
		self.messagebox = self.scrtxtfield(4, 0, 5)
		self.messageboxAddText("Messages of the Arduino")
		self.messageboxAddText("-----------------------")

		self.mainloop()

	def messageboxAddText(self, text):
		self.messagebox.configure(state = "normal")
		text = text + "\n"
		self.messagebox.insert("end", text)
		self.messagebox.configure(state = "disabled")

	def makeconnection(self):
		conn = self.connect2arduino()

	def brightnessString(self, brightnessvalue):
		if brightnessvalue == 0:
			bstring = "000"
		elif brightnessvalue > 99 and brightnessvalue <= 225:
			bstring = str(brightnessvalue)
		elif brightnessvalue > 225:
			bstring = "255"
		else:
			bstring = "0"+str(brightnessvalue)

		return bstring

	def darker_1(self):
		if self.brightness_1 > 25:
			self.brightness_1 = self.brightness_1 - 25
			self.vtxt_1.set(str(self.brightness_1))
			bstring =  self.brightnessString(self.brightness_1)
			self.comm2ardino("1"+bstring)

	def brighter_1(self):
		if self.brightness_1 == 0: self.brightness_1 = 25
		if self.brightness_1 < 250:
			self.brightness_1 = self.brightness_1 + 25
			self.vtxt_1.set(str(self.brightness_1))
			bstring =  self.brightnessString(self.brightness_1)
			self.comm2ardino("1"+bstring)
		if self.brightness_1 == 250:
			self.vtxt_1.set(str(255))
		if self.stopbutton1["text"] == "on":
			self.stopbutton1["text"] = "off"
			self.stop1 = 0

	def stop_1(self):
		if self.stop1 == 0:
			self.brightness_1 = 0
			self.vtxt_1.set(str(self.brightness_1))
			bstring =  self.brightnessString(self.brightness_1)
			self.comm2ardino("1"+bstring)
			self.stopbutton1["text"] = "on"
			self.stop1 = 1
		else:
			self.brightness_1 = 125
			self.vtxt_1.set(str(self.brightness_1))
			bstring =  self.brightnessString(self.brightness_1)
			self.comm2ardino("1"+bstring)
			self.stopbutton1["text"] = "off"
			self.stop1 = 0

	def darker_2(self):
		if self.brightness_2 > 25:
			self.brightness_2 = self.brightness_2 - 25
			self.vtxt_2.set(str(self.brightness_2))
			bstring =  self.brightnessString(self.brightness_2)
			self.comm2ardino("2"+bstring)

	def brighter_2(self):
		if self.brightness_2 == 0: self.brightness_2 = 25
		if self.brightness_2 < 250:
			self.brightness_2 = self.brightness_2 + 25
			self.vtxt_2.set(str(self.brightness_2))
			bstring =  self.brightnessString(self.brightness_2)
			self.comm2ardino("2"+bstring)
		if self.brightness_2 == 250:
			self.vtxt_2.set(str(255))
		if self.stopbutton2["text"] == "on":
			self.stopbutton2["text"] = "off"
			self.stop2 = 0

	def stop_2(self):
		if self.stop2 == 0:
			self.brightness_2 = 0
			self.vtxt_2.set(str(self.brightness_2))
			bstring =  self.brightnessString(self.brightness_2)
			self.comm2ardino("2"+bstring)
			self.stopbutton2["text"] = "on"
			self.stop2 = 1
		else:
			self.brightness_2 = 125
			self.vtxt_2.set(str(self.brightness_2))
			bstring =  self.brightnessString(self.brightness_2)
			self.comm2ardino("2"+bstring)
			self.stopbutton2["text"] = "off"
			self.stop2 = 0

	def arduinoinfo(self):
		self.comm2ardino("vvv")

	def finishprogram(self):
		"""Destroys the frame"""
		self.endframe()
		"""Destroys the dialog"""
		self.dia.destroy()

	def connect2arduino(self):
		locations=['/dev/ttyUSB0','/dev/ttyUSB1','/dev/ttyUSB2','/dev/ttyUSB3',
'/dev/ttyS0','/dev/ttyS1','/dev/ttyS2','/dev/ttyS3']  
  
		for device in locations:  
			try:  
				self.messageboxAddText("Trying "+device)
				self.arduino = serial.Serial(device, 9600)
				self.messageboxAddText("successfully!")
				return 1
			except:  
				self.messageboxAddText("Failed to connect on "+device)
				return 0

	def comm2ardino(self, commandstr):
		try:  
			self.arduino.write(commandstr.encode('utf-8'))
			self.arduinoanswer()
			if commandstr == "vvv":
				self.arduino.write("".encode('utf-8'))
				self.arduinoanswer()
		except:   
			self.messageboxAddText("Failed to send!")

	def arduinoanswer(self):
		sleep(0.5)
		answer = self.arduino.readline()
		answer = answer.decode("utf-8", "ignore")
		self.messageboxAddText(answer)


def main():
	gui = LEDsGui()
	gui.dia()
	return 0


if __name__ == '__main__':
	main()

Anmerkungen

Dieses Skript arbeitet mit dem zuvor vorgestellten Sketch "MoreLEDs.ino" zusammen, wenn sich dessen Kompilat auf dem Arduino befindet.

Zu importieren sind die Bibliotheken "serial" und "time", aus welcher aber nur "sleep" benötigt wird. Ferner sind Importe aus der Bibliothek "tkinter" für die GUI vorzunehmen.

Die Klasse "DialogMaker" enhält vorgefertigte und vorkonfigurierte Elemente für die benutzeroberfläche. Sie wurde hier auf die notwendigen Methoden reduziert.

Die Klasse "LEDsGui" erbt von der Klasse "DialogMaker" und enthält die speziellen Methoden für dieses Programm.

Die Methode "dia" erzeugt den oben abgebildeten Dialog. Unter dem Menüpunkt "Tasks" befinden sich die Unterpunkte "Connect to Arduino", "Arduino info" und "Close".

Die Klasse "LEDsGui" enthält auch die Methoden "connect2arduino", "comm2ardino" und "arduinoanswer", die den oben beschriebenen Funktionen entsprechen.

Ausblick

Die vorstehenden Programme stellen nicht mehr als einen ersten "Proof of Concept" dar.

Die Spezifikation von Schnittstellen für die Kommunikation zwischen Rechner und Arduino erscheint sinnvoll.

Soll der Rechner (eventuell ein Tablet-Computer) im Rahmen von FYMT mittels WLAN statt der Fernbedienung Steuerungsaufgaben übernehmen oder via USB solche Spurweiten steuern, die derzeit noch eine Stromzufuhr über Schiene erfordern (Nenngrößen N und Z), können hierfür mit Python3 grafische Benutzeroberflächen geschaffen werden.

Gleiches gilt hinsichtlich der Steuerung der Beleuchtung und anderer ortsfester Elemente der Modellbahnlandschaft.

Fortsetzung

Das Konzept von FYMT

Dokumentation zu FYMT