Gå til innhold
  • Bli medlem
Støtt hjemmeautomasjon.no!

GPS-Tracker for robotklipper med ESP32/uPython, NEO-6 GPS og Node-RED


SveinHa

Anbefalte innlegg

Når en har en meget komplisert plen på 750 m2 og robotklipper uten sporing så dukker der jo fort opp behov for litt snekring på gutterommet... Har hatt meg en del turer rundt i hagen for å lete etter klipperen som har satt seg fast en eller annen plass så litt hjelp til å lete og, ikke minst, se hvilke områder som klippes er ganske greit...

 

Nå har jeg jobbet noe tid med dette og det er på tide å dokumentere litt av prosessen i tilfelle andre vil gjøre noe lignende...

 

1: Skaffe kart over hagen.

Her har jeg brukt kommunekart.com og "Tegn i kart" for å lage et omriss av plenen. Etter omrisset er laget lagres det til fil i kml format. For enkelhets skyld konverterer jeg denne til .geojson format i online konverteringsprogram.

 

Pr i dag ser omrisset slik ut i kommunekart.com:

image.png.03294be598a201c0b50fa428b218b951.png

 

.geojson filen importeres manuelt i Node-RED. Dette er jo en mer eller mindre engangs sak...

 

2: Innhente GPS-data og sende til MQTT.

ESP32 med NEO-6 GPS koster "next to nothing" og er en grei løsning for dette bruket. Har slitt en del med Arduino-IDE så derfor havnet jeg på MicroPython denne gang.

 

Siden jeg har 2 separate prosesser gående samtidig har jeg valgt å bruke uasyncio for multitasking.

Task#1: Innhente data fra GPS og lagre posisjonsdata i fil.

Task#2: På gitte intervall, pt hvert 30 sek, koble på wlan og sende data fra datafilen til MQTT broker. I hagen min er der noen wlan dødsoner og derfor nyttig å gjøre opplasting litt i rykk og napp når wlan er tilgjengelig.

 

3: Behandle GPS-data i Node-RED.

Etterhvert som data mottas fra MQTT lagres de i et array of objects i flow-variabel og deretter tegnes det opp i en chart node. Array med data begrenses til et passende antall posisjoner, pt. 3000. Eldre data slettes automatisk.

 

Prototype GPS-tracker:

Robertino_GPS_prototype.thumb.jpeg.8aaaab39f64767d0bb366602affb0aa8.jpeg

 

Første sporing:

Robertino_GPS1.thumb.png.de702c3ff93cba84ad1e7af866e2b647.png

 

Node-RED:

image.thumb.png.e122efa27e438e65d0c4a1ece010e0d2.png

[{"id":"7b7be9e90c83eaff","type":"group","z":"c0e718067b85a8cc","name":"Lagre GPS-data","style":{"label":true},"nodes":["43d583213bfaa02f","2218191fe090798b","55b21eb55f283b61","8c73af56d7fdc323","36f55a2e222b2326","c466d510d0476f0e","93ac548a0bcda12d"],"x":24,"y":779,"w":832,"h":192},{"id":"43d583213bfaa02f","type":"mqtt in","z":"c0e718067b85a8cc","g":"7b7be9e90c83eaff","name":"","topic":"Ambrogio/Robertino/pos","qos":"2","datatype":"auto-detect","broker":"6db118ed1b0c56de","nl":false,"rap":true,"rh":0,"inputs":0,"x":160,"y":830,"wires":[["2218191fe090798b","55b21eb55f283b61","36f55a2e222b2326"]]},{"id":"2218191fe090798b","type":"show-value","z":"c0e718067b85a8cc","g":"7b7be9e90c83eaff","name":"","path":"","x":160,"y":880,"wires":[[]]},{"id":"55b21eb55f283b61","type":"function","z":"c0e718067b85a8cc","g":"7b7be9e90c83eaff","name":"GPS Tracker position","func":"let xMax = 5.229248;\nlet xMin = 5.228394;\nlet yMax = 59.392793;\nlet yMin = 59.392350;\nconst numpos = 4000;\nlet arr = flow.get(\"Robertino\") || []\nlet arr2 = []\n//node.warn(msg.payload);\nlet data = msg.payload;\n//var temp=data.split(\",\");\nlet msg2 = {}\n//node.warn(data);\n\n// lon/lat har format ddmm.mmmmmm\nlet lat = Number(data.lat) / 100;\n//node.warn(lat)\nlet intpart = parseInt(lat);\nlet fractpart = lat - intpart;\nlat = intpart + (fractpart * 1.666667);\n//node.warn(lat)\n\nlet lon = Number(data.lon) / 100;\nintpart = parseInt(lon);\nfractpart = lon - intpart;\nlon = intpart + (fractpart * 1.666667);\n\nlet time = data.utc;\n//node.warn(lat);\n//node.warn(lon);\n\n// Ignorer verdier utenfor kartet\nif (lat < yMax && lat > yMin && lon < xMax && lon > xMin) arr.push({\"lat\": lat, \"lon\": lon, \"UTC\": time});\n\n// Remove old positions\nlet n = arr.length;\n//node.warn(n);\narr2 = arr.slice(n - numpos);\n\n//node.warn(arr);\n//node.warn(arr2);\nflow.set(\"Robertino\", arr2);\nmsg.payload = \" \";\nmsg2.topic = \"Robertino\";\nmsg2.payload = time;\nreturn [msg, msg2];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":830,"wires":[["c466d510d0476f0e"],["8c73af56d7fdc323"]]},{"id":"8c73af56d7fdc323","type":"ui_text","z":"c0e718067b85a8cc","g":"7b7be9e90c83eaff","group":"a7f439d7b1e0bbd4","order":9,"width":"8","height":"1","name":"Robertino last track","label":"{{msg.topic}} last trck","format":"<font size=6>{{msg.payload}}<font size=3> UTC","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":740,"y":870,"wires":[]},{"id":"36f55a2e222b2326","type":"debug","z":"c0e718067b85a8cc","g":"7b7be9e90c83eaff","name":"debug 180","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"","statusType":"counter","x":160,"y":930,"wires":[]},{"id":"c466d510d0476f0e","type":"function","z":"c0e718067b85a8cc","g":"7b7be9e90c83eaff","name":"Reset delay","func":"//msg.delay = 600000;\nvar m1 = {reset:true};\nreturn [[m1,msg]];","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":610,"y":820,"wires":[["93ac548a0bcda12d"]]},{"id":"93ac548a0bcda12d","type":"delay","z":"c0e718067b85a8cc","g":"7b7be9e90c83eaff","name":"","pauseType":"delay","timeout":"2","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":770,"y":820,"wires":[["3f628673e320b6ef"]]},{"id":"6db118ed1b0c56de","type":"mqtt-broker","name":"DaleMQTT","broker":"172.16.0.94","port":"1883","clientid":"34567890","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":false,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"a7f439d7b1e0bbd4","type":"ui_group","name":"Status","tab":"55b4bf40cf1e815d","order":2,"disp":true,"width":"8","collapse":false,"className":""},{"id":"55b4bf40cf1e815d","type":"ui_tab","name":"Ambrogios","icon":"android","order":23,"disabled":false,"hidden":false},{"id":"af4caa710bbb0c2a","type":"group","z":"c0e718067b85a8cc","name":"Presenter GPS data i chart","style":{"label":true},"nodes":["8e291ee416e96572","597d06bd03195022","6969dbab26c1048b","51404847151e1696","3f628673e320b6ef"],"x":24,"y":999,"w":742,"h":112},{"id":"8e291ee416e96572","type":"inject","z":"c0e718067b85a8cc","g":"af4caa710bbb0c2a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"10","topic":"","payload":"","payloadType":"date","x":140,"y":1050,"wires":[["3f628673e320b6ef"]]},{"id":"597d06bd03195022","type":"function","z":"c0e718067b85a8cc","g":"af4caa710bbb0c2a","name":"Read temp positions & parse","func":"//let poss = flow.get(\"temp\");\nlet poss = flow.get(\"Robertino\") || []\nvar data = []\nvar data0 = []\nvar data1 = []\nvar data2 = []\nvar data3 = []\nlet j = 0;\n//for (let i in poss.data[0].attributes.positions){\n//    data0.push({x: poss.data[0].attributes.positions[i].longitude,y:poss.data[0].attributes.positions[i].latitude});\nfor (let i in poss){\n    if (poss[i].lat > 59 && poss[i].lon > 5) data0.push({x: poss[i].lon,y:poss[i].lat});\n//    node.warn(poss.data[0].attributes.positions[i].longitude);\nj = i;\n}\n// Ta vare på siste pos for oransje markering plott (nåværende pos)\n//data2.push({x: poss.data[0].attributes.positions[0].longitude,y:poss.data[0].attributes.positions[0].latitude});\n//data2.push({x: poss.data[0].attributes.positions[1].longitude,y:poss.data[0].attributes.positions[1].latitude});\ndata2.push({x: poss[j-3].lon,y:poss[j-3].lat});\ndata2.push({x: poss[j-2].lon,y:poss[j-2].lat});\ndata2.push({x: poss[j-1].lon,y:poss[j-1].lat});\ndata2.push({x: poss[j].lon,y:poss[j].lat});\n\n// Omriss av plenen\ndata1 = flow.get(\"plen\");\n// Omriss av elendom\ndata3 = flow.get(\"eiendom\");\n\n\ndata[1] = data1; // Lawnlimits\ndata[3] = data3; // Lawnlimits\ndata[2] = data0; // Track\ndata[0] = data2; // Last track\n//node.warn(data[2]);\n\nmsg.payload = [{\n    \"series\": [\"A\",\"B\",\"C\",\"D\"],\n    \"xAxisID\": 'custom-x-axis',\n    \"yAxisID\": 'custom-y-axis',\n    \"data\": data,\n    \"labels\": [\"\"]\n}]\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":450,"y":1040,"wires":[["51404847151e1696"]]},{"id":"6969dbab26c1048b","type":"function","z":"c0e718067b85a8cc","g":"af4caa710bbb0c2a","name":"modify chart options","func":"/*let centX = 5.1372;//2285;\nlet xW = 0.0005;\nlet xH = 0.0005;\nlet centY = 59.2355;//3924;\nlet xMax = centX + xW;\nlet xMin = centX - xW;\nlet yMax = centY + xH;\nlet yMin = centY - xH;\n*/\nlet xMax = 5.229248;\nlet xMin = 5.228394;\nlet yMax = 59.392793;\nlet yMin = 59.392350;\n\n\nlet gridcolors = 'rgba(255, 160, 0, 0.3)'\nmsg.ui_control = {\n    options: {\n        legend: {\n            display: false\n        },\n        tooltips: {\n            enabled: false\n        },\n        scales: {\n            xAxes: [{\n                type: 'linear',\n                id: 'custom-x-axis',\n                position:'bottom',\n                padding:100,\n                gridLines:{\n                    color:gridcolors,\n                    zeroLineColor:'rgba(123, 113, 113, 0.75)',\n                    tickMarkLength:7,\n                    drawTicks:false\n                },\n                ticks: {\n                    fontColor:\"#ccc\",\n                    max: xMax,\n                    min: xMin,\n                    stepSize: 0.0001\n                   \n                }\n            }],\n            yAxes: [{\n                id: 'custom-y-axis',\n                \n                gridLines:{\n                    color:gridcolors,\n                    zeroLineColor:'red',\n                    tickMarkLength:5,\n                    drawTicks:false\n                },\n                ticks: {\n                    fontColor:\"#ccc\",\n                    max: yMax,\n                    min: yMin,\n                    stepSize: 0.0001\n                }\n            }]\n       }\n    }\n}\ndelete msg.payload\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":1070,"wires":[["51404847151e1696"]]},{"id":"51404847151e1696","type":"ui_chart","z":"c0e718067b85a8cc","g":"af4caa710bbb0c2a","name":"Plen","group":"7ab0161dbb92ab3f","order":3,"width":"30","height":"20","label":"","chartType":"line","legend":"false","xformat":"auto","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"1","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#ff7f00","#00ff00","#8585fa","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":690,"y":1040,"wires":[[]]},{"id":"3f628673e320b6ef","type":"junction","z":"c0e718067b85a8cc","g":"af4caa710bbb0c2a","x":280,"y":1050,"wires":[["597d06bd03195022","6969dbab26c1048b"]]},{"id":"7ab0161dbb92ab3f","type":"ui_group","name":"Map","tab":"55b4bf40cf1e815d","order":3,"disp":true,"width":"32","collapse":false,"className":""},{"id":"f51cf63d67a5a2e5","type":"group","z":"c0e718067b85a8cc","name":"Les geojson fil og lagre koordinater i flow.plen","style":{"label":true},"nodes":["7a8aba7d1b2233c3","c3ac6155687bd8b2","7a3a31186dd532c5","65861ab97117862e"],"x":24,"y":1139,"w":1012,"h":82},{"id":"7a8aba7d1b2233c3","type":"json","z":"c0e718067b85a8cc","g":"f51cf63d67a5a2e5","name":"","property":"payload","action":"","pretty":false,"x":670,"y":1180,"wires":[["c3ac6155687bd8b2"]]},{"id":"c3ac6155687bd8b2","type":"function","z":"c0e718067b85a8cc","g":"f51cf63d67a5a2e5","name":"Plukk ut pos fra fil og lagre i flow","func":"var plen = []\nvar eiendom = []\n\nfor (var i in msg.payload.features[0].geometry.coordinates){\n    let tempobj = {x: msg.payload.features[0].geometry.coordinates[i][0] ,y: msg.payload.features[0].geometry.coordinates[i][1]};\n    plen.push(tempobj);\n}\n//payload.features[1].geometry.coordinates[0][0]\n//payload.features[1].geometry.coordinates[0][9]\n//payload.features[1].geometry.coordinates[0][9][0]\n//payload.features[1].geometry.coordinates[0][9][1]\nfor (var i in msg.payload.features[1].geometry.coordinates[0]) {\n    let tempobj = { x: msg.payload.features[1].geometry.coordinates[0][i][0], y: msg.payload.features[1].geometry.coordinates[0][i][1] };\n    eiendom.push(tempobj);\n}\n//node.warn(plen);\nflow.set(\"plen\", plen);\nflow.set(\"eiendom\", eiendom);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":880,"y":1180,"wires":[["3f628673e320b6ef"]]},{"id":"7a3a31186dd532c5","type":"file in","z":"c0e718067b85a8cc","g":"f51cf63d67a5a2e5","name":"","filename":"/home/nodered/PlenV4Eiendom.geojson","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":390,"y":1180,"wires":[["7a8aba7d1b2233c3"]]},{"id":"65861ab97117862e","type":"inject","z":"c0e718067b85a8cc","g":"f51cf63d67a5a2e5","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":1180,"wires":[["7a3a31186dd532c5"]]}]

 

uPython programmet:

import ubinascii
import machine
from umqtt.simple import MQTTClient
from machine import UART
import network, utime, machine
import os
import uasyncio
import json

uart = UART(2, 9600)
SSID = "WLAN-SSID"
SSID_PASSWORD = "WLAN-PWD"
filenum = 0

SERVER = "172.16.0.94"
CLIENT_ID = ubinascii.hexlify(machine.unique_id())
TOPIC = "Ambrogio/Robertino/pos"
gps_target = "$GPGGA"
gps_target = gps_target.encode('ASCII')

async def readAndSend():
    global filenum
    while True:
        await uasyncio.sleep_ms(1000 * 30) #Run every 30 sec
        sta_if = network.WLAN(network.STA_IF)
        if not sta_if.isconnected():
            print('connecting to network...')
            sta_if.active(True)
            sta_if.connect(SSID, SSID_PASSWORD)
            while not sta_if.isconnected():
                print("Attempting to connect....")
                await uasyncio.sleep_ms(1000)
            print('Connected! Network config:', sta_if.ifconfig())
            mqttClient = MQTTClient(CLIENT_ID, SERVER, keepalive=60)
            mqttClient.connect()
            print(f"Connected to MQTT Broker: {SERVER}")
            locfilenum = filenum  # Store current filenumber
            filenum = filenum + 1 # New filenumber for logging
            filename = 'data'+str(locfilenum)+'.txt'
            if filename in os.listdir():
                print('Found '+ filename)
                f = open(filename, 'r')
                line = f.readline()
                while line != "":
                    print(filename+", MQTT:"+line.strip())
                    mqttClient.publish(TOPIC, str(line))
                    await uasyncio.sleep_ms(500)
                    line = f.readline()
                print("file finished")
                print("Removing ", filename)
                os.remove(filename)
            mqttClient.disconnect()
            print("Disconnect from wlan")
            sta_if.disconnect()

def left(s, amount):
    return s[:amount]
    
async def main():
    global filenum
    print("Cleanup old files...") # Delete all existing data files on startup
    for f in os.listdir():
        if left(f, 4) == "data":
            print("Deleting: ", f)
            os.remove(f)
    print("All data files deleted.")
    while True:
        if uart.any() > 0:
            await uasyncio.sleep_ms(100)
            gps_msg = uart.readline()
            if len(gps_msg) >= 60:
                gps_type = left(gps_msg, 6)
                if gps_type == gps_target:
                    utc = gps_msg[7:13]
                    lat = gps_msg[17:27]
                    lon = gps_msg[30:41]
                    newmsg = {}
                    newmsg['utc'] = utc
                    newmsg['lat'] = lat
                    newmsg['lon'] = lon
                    jmsg = json.dumps(newmsg) # Create JSON object for datafile
                    #if lat >= '5923' and lat <= '5924' and lon >= '513' and lon <= '514':
                    filename = 'data'+str(filenum)+'.txt'
                    print(filename+", GPS:"+jmsg)
                    f = open(filename, 'a')
                    f.write(jmsg)
                    f.write("\r\n")
                    f.close()
    await uasyncio.sleep_ms(200)

# Start hele sulamitten
event_loop = uasyncio.get_event_loop()
event_loop.create_task(main())
event_loop.create_task(readAndSend())
event_loop.run_forever()

 

 

ESP32 forsynes pt via USB fra en batteribank og GPS forsynes fra Vin og GND pinnene på ESP32. GPS-TX går til ESP-RX2, GPS-RX til ESP-TX2. Har sett i minst ett eksempel på nett at NEO6 GPSen forsynes fra 3.3V men spec sier at NEO6 skal ha 3.7-5V. Prøvde først med 3.3V og det virket men fikk en del mangelfulle setninger fra GPS så med 5V gikk det myyyyye bedre... Så langt ha jeg brukt en 10Ah batteribank og den holder liv i greiene i mange døgn men skal hacke meg inn på strømforsyningen i klipperen etterhvert...

Endret av SveinHa
  • Like 4
Lenke til kommentar
Del på andre sider

Bli med i samtalen

Du kan publisere innhold nå og registrere deg senere. Hvis du har en konto, logg inn nå for å poste med kontoen din.

Gjest
Skriv svar til emnet...

×   Du har limt inn tekst med formatering.   Lim inn uten formatering i stedet

  Du kan kun bruke opp til 75 smilefjes.

×   Lenken din har blitt bygget inn på siden automatisk.   Vis som en ordinær lenke i stedet

×   Tidligere tekst har blitt gjenopprettet.   Tøm tekstverktøy

×   Du kan ikke lime inn bilder direkte. Last opp eller legg inn bilder fra URL.

×
×
  • Opprett ny...

Viktig informasjon

Vi har plassert informasjonskapsler/cookies på din enhet for å gjøre denne siden bedre. Du kan justere dine innstillinger for informasjonskapsler, ellers vil vi anta at dette er ok for deg.