Skip to content

🌊 HS AMR [2019], Hauptseminar Automatisierungstechnik: Teil HMI

Notifications You must be signed in to change notification settings

dmytkost/HS-AMR-NXT4-HMI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hauptseminar Automatisierungstechnik Praktikum: Teil HMI


Vorwort

Das Human-Maschine-Interface (weiter HMI) wurde für das iOS entwickelt und aus diesem Grund kann der Quellcode der App, der fürs Android geschrieben ist, nicht benutzt werden. In der Kommunikation müssen auch Änderungen vorgenommen werden, die weiter im Abschnitt Kommunikationsschema beschrieben sind. Diese Dokumentation dient als kurze Beschreibung der Implementierung der HMI Aufgaben. Um den Inhalt der Dokumentation möglichst kurz halten zu können, wird hier auf des Arduino UNO und NXT Teile verzichtet.


Abkürzungen: BLE – Bluetooth Low Energy. UI – User Interface

Analyse der Aufgabenstellung

HMI Aufgabenstellung

Die Aufgaben des Moduls beinhalten die Aufgaben zur Steuerung des Roboters und Darstellung der vom Roboter ausgegangenen Signalen. Als Erstes sollten die Steuerungsbefehle implementiert werden. Die Befehle sind in den Unterlagen des Seminars als SCOUT/PAUSE, PARK_NOW definiert. Der weitere Schritt ist die Darstellung des Fahrzeuges auf einer Karte zusammen mit den Parklücken. Die wichtigen Daten von Sensoren sollten zur Darstellung von Abstandsvisualisierung und Fahrzeugstatus dienen. Damit der Benutzer, indem er per Touch eine Parklücke auswählt, dem Fahrzeug aufs Parken hinweisen kann, ist die Touch-Geste-Funktion zu realisieren.

Geplantes Vorgehen

Bevor mit dem Programmieren anzufangen, ist ein UI Prototyp zu entwickeln. Anhang des Prototyps wird die Darstellung der UI Elemente und ihre Funktionen geplant. Als Nächstes beginnt man im Xcode mit dem Erstellen des Layouts. Dann platziert man UI Objekte auf dem Layout und programmiert man die im Xcode. Weiter implementiert man BLE Funktionalität, realisiert RS485 Kommunikation des Arduino UNO und NXT, damit das BLE Modul die Daten bekommen und übergeben kann. Sobald die Daten in richtiger Form zur App ankommen können, implementiert man die HMI Aufgaben zum Parken des Fahrzeuges, Zeichnen der Karte und Parklücken.

Schnittstellen mit anderen Modulen

Für die Kommunikation mit den anderen Modulen stehen zwei Klassen auf dem NXT zur Verfügung. Die Klasse HmiReaderThread und zwar die Funktion processInputs() liest die Befehle vom Benutzer ab und übergibt die Parameter an die zuständigen Variablen, die sich weiter von den anderen Modulen ablesen lassen. Die Klasse HmiSenderThread und zwar die Funktion processOutputs() sendet die Daten von Sensoren an die Fernbedienung.

HMI Entwurf

UI Funktionen

Im folgenden Bild ist eine vereinfachte schematische Funktionalität des UI Konzepts dargestellt. Der Ausgang und Eingang werden abstrakt jeweils in einem Datenfluss zusammengefasst. Aus dem Bild kann man entnehmen, was die Funktionalität des UI ist und was von ihm zu erwarten ist.

Kommunikationsschema

Die Bluetooth-Verbindung zwischen dem NXT und dem iOS-Gerät kann nicht festgestellt werden. Möglicher Grund liegt daran, dass der NXT nicht zertifiziert fürs Benutzen mit iOS Geräten ist. Deshalb ist für die Implementierung der Kommunikation ein BLE Modul auf dem Arduino UNO aufzubauen. Das

Kommunikationsschema sieht dann wie folgt aus.

Nach dem folgenden Schema ist das BLE Modul zum Arduino UNO angeschlossen.

UI Prototyp

Bei der Entwicklung des UI Prototyps dienen als Inspiration die UI der mobilen Spiele wie PUBG und Apex Legends. Anhand dieser Beispiele kann man besser nachvollziehen, wohin die Haupt- und Nebenelemente besser zu platzieren sind und wie die Information auf dem Bildschirm verteilt werden muss, um den User beim Spielen nicht zu stören. Für den Prototyp wurde ein Workspace in Adobe XD erstellt, wo UI Elemente platziert und getestet werden können. Im Prototyp-Mode des Programms könnte man alle Transaktionen zwischen den Layouts testen und es gibt eine Möglichkeit, dass der Prototyp aufs Handy zu laden und gleich's auf dem Handy zu testen ist.

Workspace des Projektes im Adobe XD.


HMI Implementierung

Hier sind alle Elemente des UI beschriftet und im Folgenden wird deren Implementierung beschrieben.

MAIN VIEW

BLE switch (1)

Funktion: BLE Verbindung

Funktion: @IBAction ble(_ sender: UISwitch)

if sender.isOn {
	let alert = UIAlertController(title: "Connect?", message: "Set bluetooth connection to NXT", preferredStyle: .alert)
	alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: {action in
	    self.connect(toPeripheral: (self.myPeripheral!))
	    sender.setOn(false, animated: true)}))
	alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: {action in
	    sender.setOn(false, animated: true)}))
	self.present(alert, animated: true)
} else {
	let alert = UIAlertController(title: "Disconnect?", message: "Lose bluetooth connection to NXT", preferredStyle: .alert)
	alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: {action in
	    self.disconnect()}))
	alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: {action in
	    sender.setOn(true, animated: true)}))
	self.present(alert, animated: true)
}

Das UI Element Switch steht für die BLE Verbindung. Falls der User den Switch aktivieren möchte, bekommt er eine Meldung mit der Frage, ob er die Verbindung feststellen möchte. Beim aktiven Zustand kann die BLE Verbindung deaktiviert werden, indem man den Switch in den deaktivierten Zustand überführt.


Darstellung der Bahn (2) und Parkflächen (3)

Die Karte und alle statische Elemente des UI wurden proportional zu den realen Abmessungen der Karte gezeichnet. Hier entspricht mes dem am größten Stück der Bahn, was 7 Rechtecke lang voraussetzt. Weiter werden die nötigen Punkte anhand start und mes maßstäblich ermittelt und die nötigen UI Elemente auf der Basis der Punkte erstellt.

Funktion: createObjects()
*****Darstellung der Bahn und Parkfläche

*****Parkflächen
let leftArea: CGRect = CGRect(x: start.x-mes*1.5/6, y: start.y---mes*0.5/6, width: mes/6, height: mes*5/6)
let rightArea: CGRect = CGRect(x: start.x---mes*1.5/6, y: start.y---mes*1.5/6, width: mes/6, height: mes*3/6)
let buttomArea: CGRect = CGRect(x: start.x, y: start.y---mes*6.5/6, width: mes/3, height: mes/6)

let leftParking = UIView(frame: leftArea)
let rightParking = UIView(frame: rightArea)
let buttomParking = UIView(frame: buttomArea)

leftParking.backgroundColor = UIColor.lightGray
rightParking.backgroundColor = UIColor.lightGray
buttomParking.backgroundColor = UIColor.lightGray

self.view.addSubview(leftParking)
self.view.addSubview(rightParking)
self.view.addSubview(buttomParking)

*****Bahn
let road = UIBezierPath()

road.move(to: start)
road.addLine(to: CGPoint(x: start.x, y: start.y---mes))
road.addLine(to: CGPoint(x: start.x---mes/3, y: start.y---mes))
road.addLine(to: CGPoint(x: start.x---mes/3, y: start.y---mes*5/6))
road.addLine(to: CGPoint(x: start.x---mes/6, y: start.y---mes*5/6))
road.addLine(to: CGPoint(x: start.x---mes/6, y: start.y---mes/6))
road.addLine(to: CGPoint(x: start.x---mes/3, y: start.y---mes/6))
road.addLine(to: CGPoint(x: start.x---mes/3, y: start.y))
road.close()

let roadLayer = CAShapeLayer()
roadLayer.path = road.cgPath
roadLayer.fillColor = UIColor.clear.cgColor
roadLayer.strokeColor = UIColor.lightGray.cgColor
roadLayer.lineWidth = mes/10

let lineLayer = CAShapeLayer()
lineLayer.path = road.cgPath
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = UIColor.darkGray.cgColor
lineLayer.lineWidth = mes/45

view.layer.addSublayer(roadLayer)
view.layer.addSublayer(lineLayer)

Parklücken (4)

Als Erstes müssen die Koordinaten der Parklücken in den pt-Bereich transformiert werden. Dafür steht das Koeffizient mas = mes/180 zur Hilfe. Weiter sind alle Koordinaten mit mas zu multiplizieren und vom start abgeleitet, weil start der Startposition des Roboters auf dem Parkour entspricht.

Funktion: makeSlots()
	
let frontSlot = CGPoint(x: DetailsController.frontSlot.x * mas --- start.x, y: DetailsController.frontSlot.y * mas --- start.y)
let backSlot = CGPoint(x: DetailsController.backSlot.x * mas --- start.x, y: DetailsController.backSlot.y * mas --- start.y)
let slotIndex = DetailsController.slotIndex.value
let width: CGFloat = 50
let height: CGFloat = 50

if slotIndex != 0 {
    slot = UIButton(type: .system)
    slot.tag = slotIndex
    
    if(frontSlot.x < start.x) {
        slot.frame = CGRect(x: start.x-mes*1.5/6---mes/50, y: frontSlot.y, width: width, height: backSlot.y-frontSlot.y)
        makeAppearance(slot: slot)
    } else if (frontSlot.y > start.y---mes) {
        slot.frame = CGRect(x: frontSlot.x, y: start.y---mes*6.5/6---mes/50, width: backSlot.x-frontSlot.x, height: height)
        makeAppearance(slot: slot)
    } else if (frontSlot.x > start.x---mes/6 && frontSlot.y < start.y---mes*5/6) {
        slot.frame = CGRect(x: start.x---mes*1.5/6---mes/50, y: backSlot.y, width: width, height: frontSlot.y-backSlot.y)
        makeAppearance(slot: slot)
    }
}

Die obere Funktion bekommt die Koordinaten einer Parklücke und erstellt ein UI Element für die.

Und die unteren Funktionen gestaltet das UI Element.

Funktion: makeAppearance(slot: UIButton)

slot.layer.cornerRadius = 10
slot.backgroundColor = slotBackgroundColor
slot.setTitleColor(UIColor.black, for: .normal)
slot.titleLabel!.font = UIFont.boldSystemFont(ofSize: 30)

if DetailsController.slotStatus == 0 {
    slot.setTitle("P", for: .normal)
    slot.addTarget(self, action: #selector(parkNow), for: UIControlEvents.touchUpInside)
} else {
    slot.setTitle("–", for: .normal)
    slot.addTarget(self, action: #selector(slotAlert), for: UIControlEvents.touchUpInside)
}

self.view.addSubview(slot)

In #selector(...) wird eine bestimmte Aktion aufgerufen. Je nachdem, ob die Parklücke suitable oder not_suitable fürs Parken ist.

Im Folgenden sind die Funktionen der Aktionen in #selector(...).

Funktion: parkNow(sender: UIButton) 

if myCharasteristic != nil {
    writeValue(data: Data(_: [0x01, UInt8(sender.tag)]))
}
UIButton.animate(withDuration: 0.1, animations: {sender.transform = CGAffineTransform(scaleX: 1.1, y: 1.15)}, completion: {finish in UIButton.animate(withDuration: 0.1, animations: {sender.transform = CGAffineTransform.identity})})

In parkNow(sender: UIButton) senden wir ein Byte-Array writeValue(data: Data(bytes: [0x01, UInt8(sender.tag)]). In diesem Array ist der erste Parameter immer ein Flag zur Erkennung der Art des Befehles. Die Art der Befehle entspricht der Enum-Liste HmiPLT.Command. In diesem Fall entspricht 0x01 HmiPLT.Command.IN_SELECTED_PARKING_SLOT. Der zweite Parameter ist slotID, was unter dem Tag des Buttons gespeichert ist.

Funktion: slotAlert(sender: UIButton)

let alert = UIAlertController(title: "", message: "This parking slot is to small", preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil))
self.present(alert, animated: true, completion: nil)

Hier bekommen wir eine Warnung, dass die Parklücke not_suitable fürs Parken ist.

PARK_NOW (5) und SCOUT/PAUSE (6) Buttons

Die zwei Buttons sind erst mal im vertikalen Stack zusammengesetzt und dann sind die Stacks Constrains zum Screen View angesetzt. Das erlaubt, das Stack als ein Objekt zu manipulieren.

Im Folgenden sind die Funktionen der Buttons.

Funktion: @IBAction parking(_ sender: UIButton)

if myCharasteristic != nil {
    writeValue(data: writeParkThis)
}
UIButton.animate(withDuration: 0.1, animations: {sender.transform = CGAffineTransform(scaleX: 1.1, y: 1.15)}, completion: {finish in UIButton.animate(withDuration: 0.1, animations: {sender.transform = CGAffineTransform.identity})})

In ‌ writeValue(data: writeParkThis) ist writeParkThis ein Array [0x00, 0x01]. Der erste Parameter wie bei den Parklücken entspricht in diesem Fall HmiPLT.Command.IN_SET_MODE und der Zweite ist INxtHmi.mode.PARK_NOW‌.

Funktion: @IBAction run(_ sender: UIButton) 

if DetailsController.statusIndex.value == 0 {
    if myCharasteristic != nil {
        writeValue(data: writePause)
    }
} else {
    if myCharasteristic != nil {
        writeValue(data: writeScout)
    }
}
UIButton.animate(withDuration: 0.1, animations: {sender.transform = CGAffineTransform(scaleX: 1.1, y: 1.15)}, completion: {finish in UIButton.animate(withDuration: 0.1, animations: {sender.transform = CGAffineTransform.identity})})

Hier schickt man Befehle SCOUT/PAUSE INxtHmi.mode.SCOUT bzw. INxtHmi.mode.PAUSE‌. Im HmiReaderThread werden die Parameter abgelesen und zugeordnet.


Animation des Pfades (7) und Roboters (99)

Als Startpunkt der Animation ist RXSwift, was eine externe Bibliothek ist. Das erlaubt, die Animation als eine Reaktion auf eine Änderung einer Variable auszulösen. Auf die Syntax der RXSwift wird genauer im Abschnitt DETAILS VIEW eingegangen.

Hier werden UI Elemente für die Animation erstellt

Funktion: createObjects()

*****Roboter
robotLayer.frame = CGRect(x: start.x-mes/14, y: start.y-mes/14, width: mes/7, height: mes/7)
robotLayer.contentsGravity = CALayerContentsGravity.resizeAspect
robotLayer.contents = UIImage(named: "Robot")?.cgImage
robotLayer.zPosition = 1

view.layer.addSublayer(robotLayer)

*****Pfad für laufendes Segment
pathLayer.fillColor = UIColor.clear.cgColor
pathLayer.strokeColor = UIColor.black.cgColor
pathLayer.lineCap = .round
pathLayer.lineWidth = mes/45

view.layer.addSublayer(pathLayer)

*****Pfad für statisches Segment
pathM.move(to: CGPoint(x: start.x, y: start.y))

pathMLayer.fillColor = UIColor.clear.cgColor
pathMLayer.strokeColor = UIColor.black.cgColor
pathMLayer.lineCap = .round
pathMLayer.lineWidth = mes/45

view.layer.addSublayer(pathMLayer)

und in der folgenden Funktion animiert.

Funktion: animation()

let mas = mes/dia

let to = CGPoint(x: DetailsController.to.x * mas --- start.x, y: DetailsController.to.y * mas --- start.y)
let from = CGPoint(x: DetailsController.from.x * mas --- start.x, y: DetailsController.from.y * mas --- start.y)
let durationMove = DetailsController.step/20

let headingTo = -DetailsController.heading * .pi/180
let headingFrom = -heading * .pi/180
let durationRot = sqrt((headingTo-headingFrom)*(headingTo-headingFrom))/(2 * .pi) * 4

if(DetailsController.from != DetailsController.to) {
    
    let moveAnimation = CABasicAnimation(keyPath: "position")
    moveAnimation.fromValue = from
    moveAnimation.toValue = to
    moveAnimation.duration = durationMove
    moveAnimation.fillMode = .forwards
    moveAnimation.isRemovedOnCompletion = false
    
    let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
    rotateAnimation.fromValue = headingFrom
    rotateAnimation.toValue = headingTo
    rotateAnimation.duration = durationRot
    rotateAnimation.beginTime = durationMove
    rotateAnimation.fillMode = .forwards
    rotateAnimation.isRemovedOnCompletion = false

    print("Heading: \(DetailsController.heading)")
    let group = CAAnimationGroup()
    group.animations = [moveAnimation, rotateAnimation]
    group.duration = durationMove --- durationRot
    group.fillMode = .forwards
    group.isRemovedOnCompletion = false
    robotLayer.add(group, forKey: nil)
                
    let path = UIBezierPath()

    path.move(to: from)
    path.addLine(to: to)
    
    pathLayer.path = path.cgPath
    
    pathM.addLine(to: from)
    pathMLayer.path = pathM.cgPath

    let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
    pathAnimation.fromValue = 0
    pathAnimation.toValue = 1
    pathAnimation.duration = durationMove
    pathLayer.add(pathAnimation, forKey: nil)
    
    heading = DetailsController.heading
}

Zuerst ordnet man die Anfangs- und Endpunkte im pt-Bereich zu und definiert die Funktionen fürs Dauern der Bewegung und der Rotation des Roboters, was in beiden Fällen Strecke über Geschwindigkeit ist. Danach werden die Animationen erstellt und den passenden Sublayers zugeordnet. Group Animation besteht aus 2 nacheinander folgenden Animationen, die erlaubt, zuerst die Bewegung und danach die Rotation des Roboters schrittweise durchzuführen.


DETAILS VIEW Button (8)

Funktion: Öffnen des DETAILS VIEW

Der Segue wurde im grafischen Interface des Xcode erstellt.

DETAILS VIEW

Die BLE Eingangspuffers werden als Erstes in prozessInput(array: [UInt8]) bearbeitet und aufgeteilt. Die Variablen zur Darstellung des Status, der gefahrenen Strecke und entdeckten Parklücken werden in der RXSwift als Observable deklariert und führen einen Code aus, wenn die aktualisiert werden.

Funktion: prozessInput(array: [UInt8])

switch Int(array[0]) {
    case 4:
        DetailsController.statusIndex.accept(Int(array[1]))
    case 3:
        DetailsController.slotStatus = Int(array[1])
        DetailsController.frontSlot.x = array[3] == 1 ? CGFloat(array[4]) * (-1) : CGFloat(array[4])
        DetailsController.frontSlot.y = CGFloat(array[5])
        DetailsController.backSlot.x = array[6] == 1 ? CGFloat(array[7]) * (-1) : CGFloat(array[7])
        DetailsController.backSlot.y = CGFloat(array[8])
        DetailsController.slotIndex.accept(Int(array[2]))
    case 2:
        DetailsController.to.x = array[1] == 1 ? CGFloat(array[2]) * (-1) : CGFloat(array[2])
        DetailsController.to.y = array[3] == 1 ? CGFloat(array[4]) * (-1) : CGFloat(array[4])
        DetailsController.heading = convertHeadingTo360(value: array[5] == 1 ? Double(array[6]) * (-1) : Double(array[6]))
        DetailsController.step = Double(CGPointDistance(from: DetailsController.from, to: DetailsController.to))
        DetailsController.distanceSum ---= DetailsController.step
        DetailsController.distance.accept(DetailsController.distanceSum)
        DetailsController.from = DetailsController.to
    default:
        print("Input index doen't match the input stream...")
    }

Die Variable statusIndex, slotIndex und distance sind Observable und ändern entsprechende UI Felder. Im Case 4 wird der Status des Roboters aktualisiert. Im Case 3 wird der Buffer mit Daten von Parklücken bearbeitet. Im Case 2 wird der Buffer mit Positionsdaten des Roboters bearbeitet.


Parking slots (1)

Funktion: Anzeige der Anzahl der gefundenen Parklücke.

Funktion: viewDodLoad()

DetailsController.slotIndex.asObservable()
        .subscribe(onNext: { value in
            if DetailsController.slotIndex.value == 1 {
                self.slots.text = "\(DetailsController.slotIndex.value) slot"
            } else {
                self.slots.text = "\(DetailsController.slotIndex.value) slots"
            }
        })
        .disposed(by: bag)

Zurückgelegte Strecke (2)

Funktion: Anzeige der zurückgelegte Strecke.

Funktion: viewDodLoad()

DetailsController.distance.asObservable()
        .subscribe(onNext: { value in
            self.distance.text = "\(String(format: "%.01f", DetailsController.distance.value)) cm"
        })
        .disposed(by: bag)

Status (3)

Funktion: Anzeige des Status des Roboters

Funktion: viewDodLoad()

DetailsController.statusIndex.asObservable()
        .subscribe(onNext: { value in
            switch DetailsController.statusIndex.value {
            case 0:
                self.status.backgroundColor = UIColor.green
            case 1:
                self.status.backgroundColor = UIColor.gray
            case 2:
                self.status.backgroundColor = UIColor.black
            default:
                self.status.backgroundColor = UIColor.red
            }
        })
        .disposed(by: bag)

Unterordnung der Farben:

Case Farbe Status
0 green DRIVING
1 gray INACTIVE
2 black EXIT
default red ERROR

Schließen Button (4)

Funktion: Schließen des DETAILS VIEW und Rückführung aufs MAIN VIEW Bei diesem Button ist es wichtig, die Prozesse im MAIN VIEW bei der Öffnung und Rückkehr nicht zu unterbrechen. Dafür ist eine Funktion unwindToGlobal(segue: UIStoryboardSegue) zu deklarieren und einen Segue aufs Exit zu setzen.

@IBAction func unwindToGlobal(segue: UIStoryboardSegue) {}

Shake It

Funktion: Zurücksetzung aller dynamischen UI Objekte auf ihre Startpositionen

Schütteln, um die Animation zu löschen.


Verbesserungsmöglichkeiten

Refactoring in beiden Controllers, bis nur nötige Funktionalität der UI Elemente in den Dateien stehen bleibt.

  • BLE Funktionalität als Protokoll erstellen
  • Hilfsfunktionen als Protokoll erstellen

About

🌊 HS AMR [2019], Hauptseminar Automatisierungstechnik: Teil HMI

Topics

Resources

Stars

Watchers

Forks