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

Anbefalte innlegg

Skrevet (endret)

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 5
Skrevet (endret)

Ca 35 minutt med klipping (4000 gps posisjoner i loggen) med "nyeklipperen" (Ambrogio L75 Evolution fra 2013):

image.thumb.png.63085b9c03f5323df3576685fb977b17.png 

...med GPS piggyback:

IMG_20240922_110441_253.thumb.jpg.c5fb50affe91ba323dafceb933f10ae3.jpg

 

Pr. i dag sender jeg alle mottatte GPS-posisjoner inn til Node-RED men har en liten plan om å redusere slik at klipperen må bevege seg f.eks >1 meter eller endre kurs mer enn 45 grader før en ny posisjon sendes. Nå koster jo ikke datatrafikken noe men en reduksjon kan uansett være greit.

Endret av SveinHa
Skrevet (endret)

Dumpet borti et litt uventet problem. Tenkte å redusere datamengden over WiFi/MQTT ved å ikke logge posisjon før klipperen hadde beveget seg f.eks mer enn 1 meter eller dreid mer enn 45 grader men på breddegrader mistet jeg litt for mye presisjon ved å konvertere string fra GPS til float. 2 desimaler forsvant og det ble litt i meste laget for dette bruket. Kan nok omgå problemet med litt triksing men skulle jo helst hatt double presisjon da...

 

Quick-and-dirty løsning ble å bare logge hver femte GPRMC setning fra GPS og de ga et ganske tilfredsstillende resultat:

image.thumb.png.d42c71bd25c0a74787bc9d1b69f8d578.png

Endret av SveinHa
Skrevet

Så er GPS-Trackeren kommet et godt stykke videre. En 3D printet brakett for ESP32, NEO6-GPS og antenne som passer i et tomrom i klipperen:

image.png.ad288e4fc13f74a32a0bb97a6cffc8a3.png

 

Montert i klipperen ser det slik ut:

image.thumb.png.2875dc3c4f6e55bc94d352189c08ab34.png

 

Strømforsyning direkte fra hovedkortet slik at jeg kan resette trackeren ved å slå klipperen av/på. 24V til USB adapter og i samme slengen laget jeg en 10k/1k spenningsdeler slik at jeg får omtrentlig batterispenning rett inn i Node-RED også.

 

Nå er vel akkurat i disse dager plenklippesesongen over men når der blir litt mer tørrvær så skal jeg slippe den utpå og teste litt mer. Akkurat nå står den i snekkerbua og sender data...

  • Like 1

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.