Skip to content

Commit b13af15

Browse files
authored
Merge pull request #846 from Courseplay/786-oxbo-pea-harvester-headland-turn
786 oxbo pea harvester headland turn
2 parents baaf81b + 6eb1afe commit b13af15

7 files changed

Lines changed: 81 additions & 116 deletions

File tree

config/VehicleConfigurations.xml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ You can define the following custom settings:
8080
- lowerEarly: boolean
8181
Similar to the above, the default behavior when starting to work is lower the implement when the back of the
8282
work area reaches to row start/field edge. Setting this to true will make Courseplay lower the implement as
83-
soon as the the front reaches the row start/field edge, avoiding unworked patches with irregular implement
83+
soon as the front reaches the row start/field edge, avoiding unworked patches with irregular implement
8484
work areas such as a plow.
8585
8686
- useVehicleSizeForMarkers: boolean
@@ -163,6 +163,13 @@ You can define the following custom settings:
163163
Forced tip side index for unloading. For now only for an auger wagon.
164164
As an example the Hawe SUW 5000 has two tipside, but only the one with the pipe is needed.
165165
166+
- disablePocket: boolean
167+
Disables creating a pocket for headland turns. Some harvesters, like mostly potato and other root vegetable
168+
harvesters, are not good at making a pocket at the headland corner as they are very long but have a small
169+
working width. We automatically try to disable pocket for most of the root vegetable harvesters, but some,
170+
like the Oxbo, are not detected correctly. This setting can be used to disable the pocket creation for these
171+
harvesters.
172+
166173
-->
167174
<VehicleConfigurations>
168175
<Configurations>
@@ -195,6 +202,7 @@ You can define the following custom settings:
195202
<Configuration type="BOOL">openPipeEarly</Configuration>
196203
<Configuration type="BOOL">closePipeAfterUnload</Configuration>
197204
<Configuration type="INT">tipSideIndex</Configuration>
205+
<Configuration type="BOOL">disablePocket</Configuration>
198206
</Configurations>
199207
<!--[GIANTS]-->
200208

@@ -229,12 +237,15 @@ You can define the following custom settings:
229237
<!--\vehicles\oxbo-->
230238
<Vehicle name="mkb4TR.xml"
231239
closePipeAfterUnload = "true"
240+
disablePocket = "true"
232241
/>
233242
<Vehicle name="bp2140e.xml"
234243
closePipeAfterUnload = "true"
244+
disablePocket = "true"
235245
/>
236246
<Vehicle name="epd540E.xml"
237247
closePipeAfterUnload = "true"
248+
disablePocket = "true"
238249
/>
239250

240251
<!--vehicles\gregoire-->

scripts/Course.lua

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,10 @@ function Course:isLastWaypointIx(ix)
648648
return #self.waypoints == ix
649649
end
650650

651+
function Course:endsInReverse()
652+
return self:isReverseAt(self:getNumberOfWaypoints())
653+
end
654+
651655
function Course:print()
652656
for i = 1, #self.waypoints do
653657
local p = self.waypoints[i]
@@ -915,13 +919,13 @@ function Course:extend(length, dx, dz)
915919
for i = first, last, step do
916920
local x = lastWp.x + dx * i
917921
local z = lastWp.z + dz * i
918-
self:appendWaypoint({ x = x, z = z })
922+
self:appendWaypoint({ x = x, z = z, rev = lastWp.rev })
919923
end
920924
if length % step > 0 then
921925
-- add the remainder to make sure we extend all the way up to length
922926
local x = lastWp.x + dx * length
923927
local z = lastWp.z + dz * length
924-
self:appendWaypoint({ x = x, z = z })
928+
self:appendWaypoint({ x = x, z = z, rev = lastWp.rev })
925929
end
926930
-- enrich the waypoints we added
927931
self:enrichWaypointData(nWaypoints)

scripts/CpUtil.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ function CpUtil.getAllRootVegetables()
466466
local preparedGrowthState = fruitTypeData.preparedGrowthState
467467
local name = fruitTypeData.name
468468

469-
-- check if fruit is needs herb removement to be harvested
469+
-- check if fruit is needs herb removal to be harvested
470470
if minPreparingGrowthState ~= -1 and preparedGrowthState ~= -1 and name ~= "SUGARCANE" then
471471
local fruitType = g_fruitTypeManager:getFruitTypeByName(name)
472472
if fruitType ~= nil then

scripts/ai/strategies/AIDriveStrategyCombineCourse.lua

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,17 +1412,12 @@ function AIDriveStrategyCombineCourse:startTurn(ix)
14121412
if self.combineController:isTowed() then
14131413
self:debug('Headland turn but this is a towed harvester using normal turn maneuvers.')
14141414
AIDriveStrategyFieldWorkCourse.startTurn(self, ix)
1415-
-- The type of fruit being harvested isn't really the indicator if we can make a headland turn
1416-
-- TODO: either make disabling combine headland turns configurable, or
1417-
-- TODO: decide automatically based on the vehicle's properties, like turn radius, work width, etc.
1418-
-- and disable when such a turn does not make sense for the vehicle.
1419-
elseif self.combineController:isRootVegetableHarvester() then
1420-
self:debug('Headland turn but this harvester uses normal turn maneuvers.')
1421-
AIDriveStrategyFieldWorkCourse.startTurn(self, ix)
14221415
elseif self.course:isOnConnectingPath(ix) then
14231416
self:debug('Headland turn but this a connecting track, use normal turn maneuvers.')
14241417
AIDriveStrategyFieldWorkCourse.startTurn(self, ix)
1425-
elseif self.course:isOnOutermostHeadland(ix) and self:isTurnOnFieldActive() then
1418+
elseif self.course:isOnOutermostHeadland(ix) and self:isTurnOnFieldActive() and
1419+
not self.combineController:isRootVegetableHarvester() and
1420+
not g_vehicleConfigurations:getRecursively(self.vehicle, 'disablePocket') then
14261421
self:debug('Creating a pocket in the corner so the combine stays on the field during the turn')
14271422
self.aiTurn = CombinePocketHeadlandTurn(self.vehicle, self, self.ppc, self.proximityController, self.turnContext,
14281423
self.course, self:getWorkWidth())

scripts/ai/turns/AITurn.lua

Lines changed: 27 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,6 @@ function KTurn:onWaypointPassed(ix, course)
378378
end
379379

380380
function KTurn:startTurn()
381-
AITurn.startTurn(self)
382381
self.state = self.states.FORWARD
383382
end
384383

@@ -438,87 +437,6 @@ function KTurn:turn(dt)
438437
return gx, gz, moveForwards, maxSpeed
439438
end
440439

441-
--[[
442-
Headland turn for combines:
443-
1. drive forward to the field edge or the headland path edge
444-
2. start turning forward
445-
3. reverse straight and then align with the direction after the
446-
corner while reversing
447-
4. forward to the turn start to continue on headland
448-
]]
449-
---@class CombineHeadlandTurn : AITurn
450-
CombineHeadlandTurn = CpObject(AITurn)
451-
452-
---@param driveStrategy AIDriveStrategyFieldWorkCourse
453-
---@param turnContext TurnContext
454-
function CombineHeadlandTurn:init(vehicle, driveStrategy, ppc, proximityController, turnContext)
455-
AITurn.init(self, vehicle, driveStrategy, ppc, proximityController, turnContext, 'CombineHeadlandTurn')
456-
self:addState('FORWARD')
457-
self:addState('REVERSE_STRAIGHT')
458-
self:addState('REVERSE_ARC')
459-
self.turningRadius = AIUtil.getTurningRadius(self.vehicle)
460-
self.cornerAngleToTurn = turnContext:getCornerAngleToTurn()
461-
-- half the turn angle but not less than 45
462-
self.angleToTurnInReverse = math.max(math.pi / 4, math.abs(self.cornerAngleToTurn / 2))
463-
self.dxToStartReverseTurn = self.turningRadius - math.abs(self.turningRadius - self.turningRadius * math.cos(self.cornerAngleToTurn))
464-
end
465-
466-
function CombineHeadlandTurn:startTurn()
467-
self.state = self.states.FORWARD
468-
self:debug('Starting combine headland turn')
469-
end
470-
471-
function CombineHeadlandTurn:onWaypointChange(ix, course)
472-
-- nothing to do
473-
end
474-
475-
function CombineHeadlandTurn:onWaypointPassed(ix, course)
476-
-- nothing to do, especially because the row finishing course is still active in the PPC and we may
477-
-- pass the last waypoint which causes the turn to end and return to field work
478-
end
479-
480-
function CombineHeadlandTurn:turn(dt)
481-
local gx, gz, moveForwards, maxSpeed = AITurn.turn(self)
482-
local dx, _, dz = self.turnContext:getLocalPositionFromTurnEnd(self.vehicle:getAIDirectionNode())
483-
local angleToTurnEnd = math.abs(self.turnContext:getAngleToTurnEndDirection(self.vehicle:getAIDirectionNode()))
484-
if self.state == self.states.FORWARD then
485-
maxSpeed = self:getForwardSpeed()
486-
moveForwards = true
487-
if angleToTurnEnd > self.angleToTurnInReverse then
488-
--and not self.turnContext:isLateralDistanceLess(dx, self.dxToStartReverseTurn) then
489-
-- full turn towards the turn end direction
490-
gx, gz = self:getGoalPointForTurn(moveForwards, self.turnContext:isLeftTurn())
491-
else
492-
-- reverse until we can make turn to the turn end point
493-
self.state = self.states.REVERSE_STRAIGHT
494-
self:debug('Combine headland turn start reversing straight')
495-
end
496-
497-
elseif self.state == self.states.REVERSE_STRAIGHT then
498-
maxSpeed = self:getReverseSpeed()
499-
moveForwards = false
500-
gx, gz = self:getGoalPointForTurn(moveForwards, nil)
501-
if math.abs(dx) < 0.2 then
502-
self.state = self.states.REVERSE_ARC
503-
self:debug('Combine headland turn start reversing arc')
504-
end
505-
506-
elseif self.state == self.states.REVERSE_ARC then
507-
maxSpeed = self:getReverseSpeed()
508-
moveForwards = false
509-
gx, gz = self:getGoalPointForTurn(moveForwards, not self.turnContext:isLeftTurn())
510-
if angleToTurnEnd < math.rad(20) then
511-
self.state = self.states.ENDING_TURN
512-
self:debug('Combine headland turn forwarding again')
513-
-- lower implements here unconditionally (regardless of the direction, self:endTurn() would wait until we
514-
-- are pointing to the turn target direction)
515-
self.driveStrategy:lowerImplements()
516-
self:resumeFieldworkAfterTurn(self.turnContext.turnEndWpIx)
517-
end
518-
end
519-
return gx, gz, moveForwards, maxSpeed
520-
end
521-
522440
--[[
523441
A turn maneuver following a course (waypoints created by turn.lua)
524442
]]
@@ -586,7 +504,6 @@ end
586504
-- = if turn on field setting is off, use pathfinder turns if enabled in settings, calculated turns otherwise
587505
--
588506
function CourseTurn:startTurn()
589-
AITurn.startTurn(self)
590507
local canTurnOnField = AITurn.canTurnOnField(self.turnContext, self.vehicle, self.workWidth, self.turningRadius)
591508
if self.turnContext:isHeadlandCorner() then
592509
self:debug('Starting a headland corner turn')
@@ -750,7 +667,7 @@ function CourseTurn:generateCalculatedTurn()
750667
self.enableTightTurnOffset = true
751668
else
752669
turnManeuver = HeadlandCornerTurnManeuver(self.vehicle, self.turnContext, self.vehicle:getAIDirectionNode(),
753-
self.turningRadius, self.workWidth, self.reversingImplement, self.steeringLength)
670+
self.turningRadius, self.workWidth, self.reversingImplement, self.steeringLength)
754671
-- adjust turn course for tight turns only for headland corners by default
755672
self.forceTightTurnOffset = self.steeringLength > 0
756673
end
@@ -826,6 +743,32 @@ function CourseTurn:drawDebug()
826743
end
827744
end
828745

746+
--[[
747+
Headland turn for combines:
748+
1. drive forward to the field edge or the headland path edge
749+
2. start turning forward
750+
3. reverse straight and then align with the direction after the
751+
corner while reversing
752+
4. forward to the turn start to continue on headland
753+
]]
754+
---@class CombineHeadlandTurn : CourseTurn
755+
CombineHeadlandTurn = CpObject(CourseTurn)
756+
757+
function CombineHeadlandTurn:init(vehicle, driveStrategy, ppc, proximityController, turnContext, fieldWorkCourse,
758+
workWidth, name)
759+
CourseTurn.init(self, vehicle, driveStrategy, ppc, proximityController, turnContext, fieldWorkCourse,
760+
workWidth, name or 'CombineHeadlandTurn')
761+
end
762+
763+
function CombineHeadlandTurn:startTurn()
764+
self:debug('Starting a combine headland turn')
765+
self.turnCourse = ReedsSheppHeadlandTurnManeuver(self.vehicle, self.turnContext,
766+
self.vehicle:getAIDirectionNode(), self.turningRadius):getCourse()
767+
self.state = self.states.TURNING
768+
self.ppc:setCourse(self.turnCourse)
769+
self.ppc:initialize(1)
770+
end
771+
829772
--- A turn maneuver to recover when the vehicle is blocked by an object (tree, fence, etc) during the turn
830773
--- This should only be initiated when the state is TURNING, so after the row is finished and before starting
831774
--- the new row.

scripts/ai/turns/TurnContext.lua

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -367,19 +367,6 @@ function TurnContext:createCorner(vehicle, r)
367367
vehicle:getCpSettings().toolOffsetX:getValue())
368368
end
369369

370-
--- Course to reverse before starting a turn to make sure the turn is completely on the field
371-
--- @param vehicle table
372-
--- @param reverseDistance number distance to reverse in meters
373-
function TurnContext:createReverseWaypointsBeforeStartingTurn(vehicle, reverseDistance)
374-
local reverserNode = AIUtil.getReverserNode(vehicle)
375-
local _, _, dStart = localToLocal(reverserNode or vehicle:getAIDirectionNode(), self.workEndNode, 0, 0, 0)
376-
local waypoints = {}
377-
for d = dStart, dStart - reverseDistance - 1, -1 do
378-
local x, y, z = localToWorld(self.workEndNode, 0, 0, d)
379-
table.insert(waypoints, {x = x, y = y, z = z, rev = true})
380-
end
381-
return waypoints
382-
end
383370

384371
--- Course to end a pathfinder turn, a straight line from where pathfinder ended, into to next row,
385372
--- making sure it is long enough so the vehicle reaches the point to lower the implements on this course

scripts/ai/turns/TurnManeuver.lua

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,14 @@ end
247247
--- Get the distance between the direction node of the vehicle and the reverser node (if there is one). This
248248
--- is to make sure that when the course changes to reverse and there is a reverse node, the first reverse
249249
--- waypoint is behind the reverser node. Otherwise we'll just keep backing up until the emergency brake is triggered.
250-
function TurnManeuver:getReversingOffset()
251-
local reverserNode, debugText = AIUtil.getReverserNode(self.vehicle)
250+
---@return number|nil distance in meters to the reverser node, or nil if there is no reverser node
251+
function TurnManeuver:getReversingOffset(vehicle, vehicleDirectionNode)
252+
local reverserNode, debugText = AIUtil.getReverserNode(vehicle)
252253
if reverserNode then
253-
local _, _, dz = localToLocal(reverserNode, self.vehicleDirectionNode, 0, 0, 0)
254+
local _, _, dz = localToLocal(reverserNode, vehicleDirectionNode, 0, 0, 0)
254255
self:debug('Using reverser node (%s) distance %.1f', debugText, dz)
255256
return math.abs(dz)
256257
end
257-
return self.steeringLength
258258
end
259259

260260
--- Set implement lowering control for the end of the turn
@@ -274,7 +274,7 @@ end
274274
function TurnManeuver:adjustCourseToFitField(course, dBack, ixBeforeEndingTurnSection)
275275
self:debug('moving course back: d=%.1f', dBack)
276276
local endingTurnLength
277-
local reversingOffset = self:getReversingOffset()
277+
local reversingOffset = self:getReversingOffset(self.vehicle, self.vehicleDirectionNode) or self.steeringLength
278278
-- generate a straight reverse section first (less than 1 m step should make sure we always end up with
279279
-- at least two waypoints
280280
local courseWithReversing = Course.createFromNode(self.vehicle, self.vehicle:getAIDirectionNode(),
@@ -434,7 +434,7 @@ function AnalyticTurnManeuver:getDistanceToMoveBack(course, workWidth, distanceT
434434
-- the field, not only the center
435435
local headlandAngle = self.turnContext:getHeadlandAngle()
436436
distanceToFieldEdge = distanceToFieldEdge -
437-
-- exclude very sharp headland angles to prevent moving back ridiculously far
437+
-- exclude very sharp headland angles to prevent moving back ridiculously far
438438
((headlandAngle > math.deg(10) and headlandAngle < math.deg(170))
439439
and (workWidth / 2 / math.abs(math.tan(headlandAngle))) or 0)
440440
self:debug('dzMax=%.1f, workWidth=%.1f, spaceNeeded=%.1f, turnEndForwardOffset=%.1f, headlandAngle=%.1f, distanceToFieldEdge=%.1f', dzMax, workWidth,
@@ -465,7 +465,7 @@ end
465465
---@class LoopTurnManeuver : TurnManeuver
466466
LoopTurnManeuver = CpObject(DubinsTurnManeuver)
467467
function LoopTurnManeuver:init(vehicle, turnContext, vehicleDirectionNode, turningRadius,
468-
workWidth, steeringLength)
468+
workWidth, steeringLength)
469469
self.debugPrefix = '(LoopTurn): '
470470
TurnManeuver.init(self, vehicle, turnContext, vehicleDirectionNode, turningRadius,
471471
workWidth, steeringLength)
@@ -552,6 +552,31 @@ function ReedsSheppTurnManeuver:findAnalyticPath(vehicleDirectionNode, startXOff
552552
return course
553553
end
554554

555+
---@class ReedsSheppHeadlandTurnManeuver : TurnManeuver
556+
ReedsSheppHeadlandTurnManeuver = CpObject(TurnManeuver)
557+
558+
--- This is a headland turn (~90 degrees) for non-towed harvesters with cutter on the front. Expected to be called
559+
--- just after the cutter finished the corner, that is, the harvester should drive forward in the original direction
560+
--- until there is no fruit left. It'll then do a quick 90 degree 3 point turn to align with the new direction.
561+
function ReedsSheppHeadlandTurnManeuver:init(vehicle, turnContext, vehicleDirectionNode, turningRadius)
562+
self.vehicle = vehicle
563+
local solver = ReedsSheppSolver()
564+
-- use lateWorkStartNode since we covered the corner in the inbound direction already
565+
local path = PathfinderUtil.findAnalyticPath(solver, vehicleDirectionNode, 0, 0,
566+
turnContext.lateWorkStartNode, 0, -turnContext.backMarkerDistance, turningRadius)
567+
self.course = Course.createFromAnalyticPath(vehicle, path, true)
568+
self.course:adjustForTowedImplements(2)
569+
if self.course:endsInReverse() then
570+
-- add a little straight section to the end so we have a little buffer and don't end the turn right at
571+
-- the work start
572+
local reversingOffset = (self:getReversingOffset(vehicle, vehicleDirectionNode) or 4)
573+
self:debug('Extending course by %.1f m', reversingOffset)
574+
self.course:extend( reversingOffset + 2, -turnContext.turnEndWp.dx, -turnContext.turnEndWp.dz)
575+
end
576+
local endingTurnLength = turnContext:appendEndingTurnCourse(self.course, 0)
577+
TurnManeuver.setLowerImplements(self.course, endingTurnLength, true)
578+
end
579+
555580
---@class TurnEndingManeuver : TurnManeuver
556581
TurnEndingManeuver = CpObject(TurnManeuver)
557582

0 commit comments

Comments
 (0)