forked from Courseplay/courseplay
-
Notifications
You must be signed in to change notification settings - Fork 0
/
BaleCollectorAIDriver.lua
440 lines (402 loc) · 16.8 KB
/
BaleCollectorAIDriver.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
--[[
This file is part of Courseplay (https://github.com/Courseplay/courseplay)
Copyright (C) 2021 Peter Vaiko
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, see <http://www.gnu.org/licenses/>.
]]
--[[
A bale loader AI driver who can find and collect bales on a field
without a field course.
For unloading, it has the same behavior as the BaleLoaderAIDriver.
It also works with a bale wrapper, find and wrap all wrappable bales.
--]]
---@class BaleCollectorAIDriver : BaleLoaderAIDriver
BaleCollectorAIDriver = CpObject(BaleLoaderAIDriver)
BaleCollectorAIDriver.myStates = {
SEARCHING_FOR_NEXT_BALE = {},
WAITING_FOR_PATHFINDER = {},
DRIVING_TO_NEXT_BALE = {},
APPROACHING_BALE = {},
WORKING_ON_BALE = {},
REVERSING_AFTER_PATHFINDER_FAILURE ={}
}
function BaleCollectorAIDriver:init(vehicle)
courseplay.debugVehicle(courseplay.DBG_AI_DRIVER,vehicle,'BaleCollectorAIDriver:init()')
BaleLoaderAIDriver.init(self, vehicle)
self:initStates(BaleCollectorAIDriver.myStates)
self.mode = courseplay.MODE_BALE_COLLECTOR
self.debugChannel = courseplay.DBG_MODE_7
self.fieldId = 0
self.bales = {}
end
function BaleCollectorAIDriver:setHudContent()
-- skip the inheritance from fieldwork/bale loader as this is very special
AIDriver.setHudContent(self)
courseplay.hud:setBaleCollectorAIDriverContent(self.vehicle)
end
function BaleCollectorAIDriver:setUpAndStart(startingPoint)
-- make sure we have a good turning radius set
self.turnRadius = AIDriverUtil.getTurningRadius(self.vehicle)
-- we only have an unload course since we are driving on the field autonomously
self.unloadRefillCourse = Course(self.vehicle, self.vehicle.Waypoints, false)
-- Set the offset to 0, we'll take care of getting the grabber to the right place
self.vehicle.cp.settings.toolOffsetX:set(0)
self.pathfinderFailureCount = 0
if startingPoint:is(StartingPointSetting.START_COLLECTING_BALES) then
-- to always have a valid course (for the traffic conflict detector mainly)
self.fieldworkCourse = self:getStraightForwardCourse(25)
self:startCourse(self.fieldworkCourse, 1)
local myField = self.vehicle.cp.settings.baleCollectionField:get()
if not myField or myField < 1 then
self:stop("NO_FIELD_SELECTED")
return
end
BaleWrapperAIDriver.initializeBaleWrapper(self)
self.bales = self:findBales(myField)
self:changeToFieldwork()
self:collectNextBale()
else
local closestIx, _, closestIxRightDirection, _ =
self.unloadRefillCourse:getNearestWaypoints(AIDriverUtil.getDirectionNode(self.vehicle))
local startIx = 1
if startingPoint:is(StartingPointSetting.START_AT_NEAREST_POINT) then
startIx = closestIx
elseif startingPoint:is(StartingPointSetting.START_AT_NEXT_POINT) then
startIx = closestIxRightDirection
end
self:changeToUnloadOrRefill()
self:startCourseWithPathfinding(self.unloadRefillCourse, startIx)
end
end
function BaleCollectorAIDriver:setBaleCollectingState(state)
self.baleCollectingState = state
self:debug('baleCollectingState: %s', self.baleCollectingState.name)
end
function BaleCollectorAIDriver:collectNextBale()
self:setBaleCollectingState(self.states.SEARCHING_FOR_NEXT_BALE)
if #self.bales > 0 then
self:findPathToNextBale()
else
self:info('No bales found, scan the field once more before leaving for the unload course.')
self.bales = self:findBales(self.vehicle.cp.settings.baleCollectionField:get())
if #self.bales > 0 then
self:info('Found more bales, collecting them')
self:findPathToNextBale()
return
end
self:info('There really are no more bales on the field')
if self.baleLoader and self:getFillLevel() > 0.1 then
self:changeToUnloadOrRefill()
self:startCourseWithPathfinding(self.unloadRefillCourse, 1)
else
self:onEndCourseFinished()
end
end
end
--- Find bales on field
---@return BaleToCollect[] list of bales found
function BaleCollectorAIDriver:findBales(fieldId)
self:debug('Finding bales on field %d...', fieldId or 0)
local balesFound = {}
for _, object in pairs(g_currentMission.nodeToObject) do
if BaleToCollect.isValidBale(object, self.baleWrapper) then
local bale = BaleToCollect(object)
-- if the bale has a mountObject it is already on the loader so ignore it
if (not fieldId or fieldId == 0 or bale:getFieldId() == fieldId) and
not object.mountObject and
object:getOwnerFarmId() == self.vehicle:getOwnerFarmId()
then
-- bales may have multiple nodes, using the object.id deduplicates the list
balesFound[object.id] = bale
end
end
end
-- convert it to a normal array so lua can give us the number of entries
local bales = {}
for _, bale in pairs(balesFound) do
table.insert(bales, bale)
end
self:debug('Found %d bales on field %d', #bales, fieldId)
return bales
end
---@return BaleToCollect, number closest bale and its distance
function BaleCollectorAIDriver:findClosestBale(bales)
local closestBale, minDistance, ix = nil, math.huge
local invalidBales = 0
for i, bale in ipairs(bales) do
if bale:isStillValid() then
local _, _, _, d = bale:getPositionInfoFromNode(AIDriverUtil.getDirectionNode(self.vehicle))
self:debug('%d. bale (%d, %s) in %.1f m', i, bale:getId(), bale:getBaleObject(), d)
if d < self.settings.turnDiameter:get() * 2 then
-- if it is really close, check the length of the Dubins path
-- as we may need to drive a loop first to get to it
d = self:getDubinsPathLengthToBale(bale)
self:debug(' Dubins length is %.1f m', d)
end
if d < minDistance then
closestBale = bale
minDistance = d
ix = i
end
else
--- When a bale gets wrapped it changes its identity and the node becomes invalid. This can happen
--- when we pick up (and wrap) a bale other than the target bale, for example because there's another bale
--- in the grabber's way. That is now wrapped but our bale list does not know about it so let's rescan the field
self:debug('%d. bale (%d, %s) INVALID', i, bale:getId(), bale:getBaleObject())
invalidBales = invalidBales + 1
self:debug('Found an invalid bales, rescanning field', invalidBales)
self.bales = self:findBales(self.vehicle.cp.settings.baleCollectionField:get())
-- return empty, next time this is called everything should be ok
return
end
end
return closestBale, minDistance, ix
end
function BaleCollectorAIDriver:getDubinsPathLengthToBale(bale)
local start = PathfinderUtil.getVehiclePositionAsState3D(self.vehicle)
local goal = self:getBaleTarget(bale)
local solution = PathfinderUtil.dubinsSolver:solve(start, goal, self.turnRadius)
return solution:getLength(self.turnRadius)
end
function BaleCollectorAIDriver:findPathToNextBale()
if not self.bales then return end
local bale, d, ix = self:findClosestBale(self.bales)
if ix then
if bale:isLoaded() then
self:debug('Bale %d is already loaded, skipping', bale:getId())
table.remove(self.bales, ix)
elseif not self:isObstacleAhead() then
self:startPathfindingToBale(bale)
-- remove bale from list
table.remove(self.bales, ix)
else
self:debug('There is an obstacle ahead, backing up a bit and retry')
self:startReversing()
end
end
end
--- The trick here is to get a target direction at the bale
function BaleCollectorAIDriver:getBaleTarget(bale)
-- first figure out the direction at the goal, as the pathfinder needs that.
-- for now, just use the direction from our location towards the bale
local xb, zb, yRot, d = bale:getPositionInfoFromNode(AIDriverUtil.getDirectionNode(self.vehicle))
return State3D(xb, -zb, courseGenerator.fromCpAngle(yRot))
end
---@param bale BaleToCollect
function BaleCollectorAIDriver:startPathfindingToBale(bale)
if not self.pathfinder or not self.pathfinder:isActive() then
self.pathfindingStartedAt = self.vehicle.timer
local safeDistanceFromBale = bale:getSafeDistance()
local halfVehicleWidth = self.vehicle.sizeWidth and self.vehicle.sizeWidth / 2 or 1.5
self:debug('Start pathfinding to next bale (%d), safe distance from bale %.1f, half vehicle width %.1f',
bale:getId(), safeDistanceFromBale, halfVehicleWidth)
local goal = self:getBaleTarget(bale)
local configuredOffset = self:getConfiguredOffset()
local offset = Vector(0, safeDistanceFromBale +
(configuredOffset and configuredOffset or (halfVehicleWidth + 0.2)))
goal:add(offset:rotate(goal.t))
local done, path, goalNodeInvalid
self.pathfinder, done, path, goalNodeInvalid =
PathfinderUtil.startPathfindingFromVehicleToGoal(self.vehicle, goal, false, self.fieldId,
{}, self.lastBale and {self.lastBale} or {})
if done then
return self:onPathfindingDoneToNextBale(path, goalNodeInvalid)
else
self:setBaleCollectingState(self.states.WAITING_FOR_PATHFINDER)
self:setPathfindingDoneCallback(self, self.onPathfindingDoneToNextBale)
return true
end
else
self:debug('Pathfinder already active')
end
end
function BaleCollectorAIDriver:onPathfindingDoneToNextBale(path, goalNodeInvalid)
if path and #path > 2 then
self.pathfinderFailureCount = 0
self:debug('Found path (%d waypoints, %d ms)', #path, self.vehicle.timer - (self.pathfindingStartedAt or 0))
self.fieldworkCourse = Course(self.vehicle, courseGenerator.pointsToXzInPlace(path), true)
self:startCourse(self.fieldworkCourse, 1)
self:debug('Driving to next bale')
self:setBaleCollectingState(self.states.DRIVING_TO_NEXT_BALE)
return true
else
self.pathfinderFailureCount = self.pathfinderFailureCount + 1
if self.pathfinderFailureCount == 1 then
self:debug('Finding path to next bale failed, trying next bale')
self:setBaleCollectingState(self.states.SEARCHING_FOR_NEXT_BALE)
elseif self.pathfinderFailureCount == 2 then
if self:isNearFieldEdge() then
self.pathfinderFailureCount = 0
self:debug('Finding path to next bale failed twice, we are close to the field edge, back up a bit and then try again')
self:startReversing()
else
self:debug('Finding path to next bale failed twice, but we are not too close to the field edge, trying another bale')
self:setBaleCollectingState(self.states.SEARCHING_FOR_NEXT_BALE)
end
else
self:info('Pathfinding failed three times, giving up')
self.pathfinderFailureCount = 0
self:stop('WORK_END')
end
return false
end
end
function BaleCollectorAIDriver:startReversing()
self:startCourse(self:getStraightReverseCourse(10), 1)
self:setBaleCollectingState(self.states.REVERSING_AFTER_PATHFINDER_FAILURE)
end
function BaleCollectorAIDriver:isObstacleAhead()
-- check the proximity sensor first
if self.forwardLookingProximitySensorPack then
local d, vehicle, _, deg, dAvg = self.forwardLookingProximitySensorPack:getClosestObjectDistanceAndRootVehicle()
if d < 1.2 * self.turnRadius then
self:debug('Obstacle ahead at %.1f m', d)
return true
end
end
-- then a more thorough check, we want to ignore the last bale we worked on as that may lay around too close
-- to the baler. This happens for example to the Andersen bale wrapper.
self:debug('Check obstacles ahead, ignoring bale object %s', self.lastBale and self.lastBale or 'nil')
local leftOk, rightOk, straightOk =
PathfinderUtil.checkForObstaclesAhead(self.vehicle, self.turnRadius, self.lastBale and{self.lastBale})
-- if at least one is ok, we are good to go.
return not (leftOk or rightOk or straightOk)
end
function BaleCollectorAIDriver:isNearFieldEdge()
local x, _, z = localToWorld(AIDriverUtil.getDirectionNode(self.vehicle), 0, 0, 0)
local vehicleIsOnField = courseplay:isField(x, z, 1, 1)
x, _, z = localToWorld(AIDriverUtil.getDirectionNode(self.vehicle), 0, 0, 1.2 * self.turnRadius)
local isFieldInFrontOfVehicle = courseplay:isField(x, z, 1, 1)
self:debug('vehicle is on field: %s, field in front of vehicle: %s',
tostring(vehicleIsOnField), tostring(isFieldInFrontOfVehicle))
return vehicleIsOnField and not isFieldInFrontOfVehicle
end
function BaleCollectorAIDriver:onLastWaypoint()
if self.state == self.states.ON_FIELDWORK_COURSE and self.fieldworkState == self.states.WORKING then
if self.baleCollectingState == self.states.DRIVING_TO_NEXT_BALE then
self:debug('last waypoint while driving to next bale reached')
self:startApproachingBale()
elseif self.baleCollectingState == self.states.WORKING_ON_BALE then
self:debug('last waypoint on bale pickup reached, start collecting bales again')
self:collectNextBale()
elseif self.baleCollectingState == self.states.APPROACHING_BALE then
self:debug('looks like somehow we missed a bale, rescanning field')
self.bales = self:findBales(self.vehicle.cp.settings.baleCollectionField:get())
self:collectNextBale()
elseif self.baleCollectingState == self.states.REVERSING_AFTER_PATHFINDER_FAILURE then
self:debug('backed up after pathfinder failed, trying again')
self:setBaleCollectingState(self.states.SEARCHING_FOR_NEXT_BALE)
end
else
BaleLoaderAIDriver.onLastWaypoint(self)
end
end
function BaleCollectorAIDriver:onEndCourse()
if self.state == self.states.ON_UNLOAD_OR_REFILL_COURSE or
self.state == self.states.ON_UNLOAD_OR_REFILL_WITH_AUTODRIVE then
self:debug('Back from unload course, check for bales again')
self.bales = self:findBales(self.vehicle.cp.settings.baleCollectionField:get())
self:changeToFieldwork()
self:collectNextBale()
else
BaleLoaderAIDriver.onEndCourse(self)
end
end
function BaleCollectorAIDriver:startApproachingBale()
self:debug('Approaching bale...')
self:startCourse(self:getStraightForwardCourse(20), 1)
self:setBaleCollectingState(self.states.APPROACHING_BALE)
end
--- Called from the generic driveFieldwork(), this the part doing the actual work on the field after/before all
--- implements are started/lowered etc.
function BaleCollectorAIDriver:work()
if self.baleCollectingState == self.states.SEARCHING_FOR_NEXT_BALE then
self:setSpeed(0)
self:debug('work: searching for next bale')
self:collectNextBale()
elseif self.baleCollectingState == self.states.WAITING_FOR_PATHFINDER then
self:setSpeed(0)
elseif self.baleCollectingState == self.states.DRIVING_TO_NEXT_BALE then
self:setSpeed(self.vehicle:getSpeedLimit())
elseif self.baleCollectingState == self.states.APPROACHING_BALE then
self:setSpeed(self:getWorkSpeed() / 2)
self:approachBale()
elseif self.baleCollectingState == self.states.WORKING_ON_BALE then
self:workOnBale()
self:setSpeed(0)
elseif self.baleCollectingState == self.states.REVERSING_AFTER_PATHFINDER_FAILURE then
self:setSpeed(self.vehicle.cp.speeds.reverse)
end
self:checkFillLevels()
end
function BaleCollectorAIDriver:approachBale()
if self.baleLoader then
if self.baleLoader.spec_baleLoader.grabberMoveState then
self:debug('Start picking up bale')
self:setBaleCollectingState(self.states.WORKING_ON_BALE)
end
end
if self.baleWrapper then
BaleWrapperAIDriver.handleBaleWrapper(self)
if self.baleWrapper.spec_baleWrapper.baleWrapperState ~= BaleWrapper.STATE_NONE then
self:debug('Start wrapping bale')
self:setBaleCollectingState(self.states.WORKING_ON_BALE)
end
end
end
function BaleCollectorAIDriver:workOnBale()
if self.baleLoader then
if not self.baleLoader.spec_baleLoader.grabberMoveState then
self:debug('Bale picked up, moving on to the next')
self:collectNextBale()
end
end
if self.baleWrapper then
BaleWrapperAIDriver.handleBaleWrapper(self)
if self.baleWrapper.spec_baleWrapper.baleWrapperState == BaleWrapper.STATE_NONE then
self.lastBale = self.baleWrapper.spec_baleWrapper.lastDroppedBale
self:debug('Bale wrapped, moving on to the next, last dropped bale %s', self.lastBale)
self:collectNextBale()
end
end
end
function BaleCollectorAIDriver:calculateTightTurnOffset()
self.tightTurnOffset = 0
end
function BaleCollectorAIDriver:getConfiguredOffset()
if self.baleLoader then
return g_vehicleConfigurations:get(self.baleLoader, 'baleCollectorOffset')
elseif self.baleWrapper then
return g_vehicleConfigurations:get(self.baleWrapper, 'baleCollectorOffset')
end
end
function BaleCollectorAIDriver:getFillLevel()
local fillLevelInfo = {}
self:getAllFillLevels(self.vehicle, fillLevelInfo)
for fillType, info in pairs(fillLevelInfo) do
if fillType == FillType.SQUAREBALE or
fillType == FillType.SQUAREBALE_WHEAT or
fillType == FillType.SQUAREBALE_BARLEY or
fillType == FillType.ROUNDBALE or
fillType == FillType.ROUNDBALE_WHEAT or
fillType == FillType.ROUNDBALE_BARLEY or
fillType == FillType.ROUNDBALE_GRASS or
fillType == FillType.ROUNDBALE_DRYGRASS then
return info.fillLevel
end
end
end
function BaleCollectorAIDriver:isAutoContinueAtWaitPointEnabled()
return true
end
function BaleCollectorAIDriver:isStoppingAtWaitPointAllowed()
return true
end