forked from Courseplay/courseplay
-
Notifications
You must be signed in to change notification settings - Fork 0
/
FieldworkAIDriver.lua
1314 lines (1190 loc) · 52 KB
/
FieldworkAIDriver.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
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--[[
This file is part of Courseplay (https://github.com/Courseplay/courseplay)
Copyright (C) 2018 Peter Vajko
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/>.
]]
--[[
Fieldwork AI Driver
Can follow a fieldworking course, perform turn maneuvers, turn on/off and raise/lower implements,
add adjustment course if needed.
]]
---@class FieldworkAIDriver : AIDriver
FieldworkAIDriver = CpObject(AIDriver)
FieldworkAIDriver.myStates = {
ON_FIELDWORK_COURSE = {},
WORKING = {},
ON_UNLOAD_OR_REFILL_COURSE = {},
RETURNING_TO_FIRST_POINT = {},
UNLOAD_OR_REFILL_ON_FIELD = {},
WAITING_FOR_UNLOAD_OR_REFILL ={}, -- while on the field
ON_CONNECTING_TRACK = {},
WAITING_FOR_LOWER = {},
WAITING_FOR_LOWER_DELAYED = {},
WAITING_FOR_STOP = {},
ON_UNLOAD_OR_REFILL_WITH_AUTODRIVE = {},
TURNING = {},
FORWARD1 = {},
FORWARD2 = {},
REVERSE = {},
}
-- Our class implementation does not call the constructor of base classes
-- through multiple level of inheritances therefore we must explicitly call
-- the base class ctr.
function FieldworkAIDriver:init(vehicle)
courseplay.debugVehicle(11,vehicle,'FieldworkAIDriver:init()')
AIDriver.init(self, vehicle)
self:initStates(FieldworkAIDriver.myStates)
-- waiting for tools to turn on, unfold and lower
self.waitingForTools = true
self.debugChannel = 14
-- waypoint index on main (fieldwork) course where we aborted the work before going on
-- an unload/refill course
self.aiDriverData.continueFieldworkAtWaypoint = 1
-- force stop for unload/refill, for example by a tractor, otherwise the same as stopping because full or empty
self.heldForUnloadRefill = false
self.heldForUnloadRefillTimestamp = 0
-- stop and raise implements while refilling/unloading on field
self.stopImplementsWhileUnloadOrRefillOnField = true
-- duration of the last turn maneuver. This is a default value and the driver will measure
-- the actual turn times. Used to calculate the remaining fieldwork time
self.turnDurationMs = 20000
end
function FieldworkAIDriver:setHudContent()
AIDriver.setHudContent(self)
courseplay.hud:setFieldWorkAIDriverContent(self.vehicle)
end
function FieldworkAIDriver.register()
AIImplement.getCanImplementBeUsedForAI = Utils.overwrittenFunction(AIImplement.getCanImplementBeUsedForAI,
function(self, superFunc)
if SpecializationUtil.hasSpecialization(BaleLoader, self.specializations) then
return true
elseif SpecializationUtil.hasSpecialization(BaleWrapper, self.specializations) then
return true
elseif SpecializationUtil.hasSpecialization(Pickup, self.specializations) then
return true
elseif superFunc ~= nil then
return superFunc(self)
end
end)
-- Make sure the Giants helper can't be hired for implements which have no Giants AI functionality
AIVehicle.getCanStartAIVehicle = Utils.overwrittenFunction(AIVehicle.getCanStartAIVehicle,
function(self, superFunc)
-- Only the courseplay helper can handle bale loaders.
if FieldworkAIDriver.hasImplementWithSpecialization(self, BaleLoader) or
FieldworkAIDriver.hasImplementWithSpecialization(self, BaleWrapper) or
FieldworkAIDriver.hasImplementWithSpecialization(self, Pickup) then
return false
end
if superFunc ~= nil then
return superFunc(self)
end
end)
BaleLoaderAIDriver.register()
Pickup.onAIImplementStartLine = Utils.overwrittenFunction(Pickup.onAIImplementStartLine,
function(self, superFunc)
if superFunc ~= nil then superFunc(self) end
self:setPickupState(true)
end)
Pickup.onAIImplementEndLine = Utils.overwrittenFunction(Pickup.onAIImplementEndLine,
function(self, superFunc)
if superFunc ~= nil then superFunc(self) end
self:setPickupState(false)
end)
-- TODO: move these to another dedicated class for implements?
local PickupRegisterEventListeners = function(vehicleType)
print('## Courseplay: Registering event listeners for loader wagons.')
SpecializationUtil.registerEventListener(vehicleType, "onAIImplementStartLine", Pickup)
SpecializationUtil.registerEventListener(vehicleType, "onAIImplementEndLine", Pickup)
end
print('## Courseplay: Appending event listener for loader wagons.')
Pickup.registerEventListeners = Utils.appendedFunction(Pickup.registerEventListeners, PickupRegisterEventListeners)
end
function FieldworkAIDriver.hasImplementWithSpecialization(vehicle, specialization)
return FieldworkAIDriver.getImplementWithSpecialization(vehicle, specialization) ~= nil
end
function FieldworkAIDriver.getImplementWithSpecialization(vehicle, specialization)
local aiImplements = vehicle:getAttachedAIImplements()
for _, implement in ipairs(aiImplements) do
if SpecializationUtil.hasSpecialization(specialization, implement.object.specializations) then
return implement.object
end
end
end
--- Start the course and turn on all implements when needed
function FieldworkAIDriver:start(ix)
self:debug('Starting in mode %d', self.mode)
self.ppc:registerListeners(self, 'onWaypointPassed', 'onWaypointChange')
self:setMarkers()
self:beforeStart()
-- time to lower all implements
self:findLoweringDurationMs()
-- always enable alignment with first waypoint, this is needed to properly start/continue fieldwork
self.alignmentEnabled = self.vehicle.cp.alignment.enabled
self.vehicle.cp.alignment.enabled = true
-- stop at the last waypoint by default
self.vehicle.cp.stopAtEnd = true
-- any offset imposed by the driver itself (tight turns, end of course, etc.), addtional to any
-- tool offsets
self.aiDriverOffsetX = 0
self.aiDriverOffsetZ = 0
self:setUpCourses()
self.ppc:setNormalLookaheadDistance()
self.waitingForTools = true
-- on which course are we starting?
-- the ix we receive here is the waypoint index in the fieldwork course and the unload/fill
-- course concatenated.
if ix > self.fieldworkCourse:getNumberOfWaypoints() then
-- beyond the first, fieldwork course: we are on the unload/refill part
self:changeToUnloadOrRefill()
self:startCourseWithAlignment(self.unloadRefillCourse, ix - self.fieldworkCourse:getNumberOfWaypoints())
else
-- we are on the fieldwork part
self:startFieldworkWithPathfinding(ix)
end
end
function FieldworkAIDriver:startFieldworkWithAlignment(ix)
if self:startCourseWithAlignment(self.fieldworkCourse, ix) then
self.state = self.states.ON_FIELDWORK_COURSE
self.fieldworkState = self.states.TEMPORARY
else
self:changeToFieldwork()
end
end
function FieldworkAIDriver:startFieldworkWithPathfinding(ix)
if self:startCourseWithPathfinding(self.fieldworkCourse, ix) then
self.state = self.states.ON_FIELDWORK_COURSE
self.fieldworkState = self.states.TEMPORARY
else
self:changeToFieldwork()
end
end
function FieldworkAIDriver:stop(msgReference)
self:stopWork()
AIDriver.stop(self, msgReference)
-- Restore alignment settings. TODO: remove this setting from the HUD and always enable it
self.vehicle.cp.alignment.enabled = self.alignmentEnabled
end
function FieldworkAIDriver:drive(dt)
courseplay:updateFillLevelsAndCapacities(self.vehicle)
if self.state == self.states.ON_FIELDWORK_COURSE then
if self:driveFieldwork(dt) then
-- driveFieldwork is driving, no need for AIDriver
return
end
elseif self.state == self.states.ON_UNLOAD_OR_REFILL_COURSE then
if self:driveUnloadOrRefill(dt) then
-- someone else is driving, no need to call AIDriver.drive()
return
end
elseif self.state == self.states.RETURNING_TO_FIRST_POINT then
self:setSpeed(self:getFieldSpeed())
elseif self.state == self.states.ON_UNLOAD_OR_REFILL_WITH_AUTODRIVE then
-- AutoDrive is driving, don't call AIDriver.drive()
return
end
self:setRidgeMarkers()
self:resetUnloadOrRefillHold()
AIDriver.drive(self, dt)
self:measureTurnTime()
end
-- Hold for unload (or refill) for example a combine can be asked by a an unloading tractor
-- to stop and wait. Must be called in every loop to keep waiting because it will automatically be
-- reset and the vehicle restarted. This way the users don't explicitly need to call resumeAfterUnloadOrRefill()
function FieldworkAIDriver:holdForUnloadOrRefill()
self.heldForUnloadRefill = true
self.heldForUnloadRefillTimestamp = g_updateLoopIndex
end
function FieldworkAIDriver:resumeAfterUnloadOrRefill()
self.heldForUnloadRefill = false
end
function FieldworkAIDriver:resetUnloadOrRefillHold()
if g_updateLoopIndex > self.heldForUnloadRefillTimestamp + 10 then
self:resumeAfterUnloadOrRefill()
end
end
--- Doing the fieldwork (headlands or up/down rows, including the turns)
---@return boolean true if driveFieldwork() is driving (no need to call ALDriver.drive())
function FieldworkAIDriver:driveFieldwork(dt)
local iAmDriving = false
self:updateFieldworkOffset()
if self.fieldworkState == self.states.WAITING_FOR_LOWER_DELAYED then
-- getCanAIVehicleContinueWork() seems to return false when the implement being lowered/raised (moving) but
-- true otherwise. Due to some timing issues it may return true just after we started lowering it, so this
-- here delays the check for another cycle.
self.fieldworkState = self.states.WAITING_FOR_LOWER
elseif self.fieldworkState == self.states.WAITING_FOR_LOWER then
if self.vehicle:getCanAIVehicleContinueWork() then
self:debug('all tools ready, start working')
self.fieldworkState = self.states.WORKING
self:setSpeed(self:getWorkSpeed())
else
self:debug('waiting for all tools to lower')
self:setSpeed(0)
self:checkFillLevels()
end
elseif self.fieldworkState == self.states.WORKING then
self:setSpeed(self:getWorkSpeed())
self:manageConvoy()
self:checkWeather()
self:checkFillLevels()
elseif self.fieldworkState == self.states.UNLOAD_OR_REFILL_ON_FIELD then
self:driveFieldworkUnloadOrRefill()
elseif self.fieldworkState == self.states.TEMPORARY then
self:setSpeed(self:getFieldSpeed())
elseif self.fieldworkState == self.states.ON_CONNECTING_TRACK then
self:setSpeed(self:getFieldSpeed())
elseif self.fieldworkState == self.states.TURNING and self.aiTurn then
iAmDriving = self.aiTurn:drive(dt)
end
return iAmDriving
end
function FieldworkAIDriver:checkFillLevels()
if not self:allFillLevelsOk() or self.heldForUnloadRefill then
self:stopAndChangeToUnload()
end
end
function FieldworkAIDriver:stopAndChangeToUnload()
if self.unloadRefillCourse and not self.heldForUnloadRefill then
self:rememberWaypointToContinueFieldwork()
self:debug('at least one tool is empty/full, aborting work at waypoint %d.', self.aiDriverData.continueFieldworkAtWaypoint or -1)
self:changeToUnloadOrRefill()
self:startCourseWithPathfinding(self.unloadRefillCourse, 1)
else
if self.vehicle.cp.settings.autoDriveMode:useForUnloadOrRefill() then
-- Switch to AutoDrive when enabled
self:rememberWaypointToContinueFieldwork()
self:stopWork()
self:foldImplements()
self.state = self.states.ON_UNLOAD_OR_REFILL_WITH_AUTODRIVE
self:debug('passing the control to AutoDrive to run the unload/refill course.')
self.vehicle.spec_autodrive:StartDrivingWithPathFinder(self.vehicle, self.vehicle.ad.mapMarkerSelected, self.vehicle.ad.mapMarkerSelected_Unload, self, FieldworkAIDriver.onEndCourse, nil);
else
-- otherwise we'll
self:changeToFieldworkUnloadOrRefill()
end;
end
end
---@return boolean true if unload took over the driving
function FieldworkAIDriver:driveUnloadOrRefill()
if self.course:isTemporary() then
-- use the courseplay speed limit until we get to the actual unload course fields (on alignment/temporary)
self:setSpeed(self.vehicle.cp.speeds.field)
else
-- just drive normally
self:setSpeed(self:getRecordedSpeed())
end
-- except when in reversing, then always use reverse speed
if self.ppc:isReversing() then
self:setSpeed(self.vehicle.cp.speeds.reverse or self.vehicle.cp.speeds.crawl)
end
return false
end
--- Full during fieldwork
function FieldworkAIDriver:changeToFieldworkUnloadOrRefill()
self.fieldworkState = self.states.UNLOAD_OR_REFILL_ON_FIELD
if self.stopImplementsWhileUnloadOrRefillOnField then
self.fieldWorkUnloadOrRefillState = self.states.WAITING_FOR_STOP
else
self.fieldWorkUnloadOrRefillState = self.states.WAITING_FOR_UNLOAD_OR_REFILL
end
end
--- Stop for unload/refill while driving the fieldwork course
function FieldworkAIDriver:driveFieldworkUnloadOrRefill()
-- don't move while empty
self:setSpeed(0)
if self.fieldWorkUnloadOrRefillState == self.states.WAITING_FOR_STOP then
-- wait until we stopped before raising the implements
if self:isStopped() then
self:debug('implements raised, stop')
self:stopWork()
self.fieldWorkUnloadOrRefillState = self.states.WAITING_FOR_UNLOAD_OR_REFILL
end
elseif self.fieldWorkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_OR_REFILL then
if self:allFillLevelsOk() and not self.heldForUnloadRefill then
self:debug('unloaded, continue working')
-- not full/empty anymore, maybe because Refilling to a trailer, go back to work
self:clearInfoText(self:getFillLevelInfoText())
self:changeToFieldwork()
end
end
end
function FieldworkAIDriver:changeToFieldwork()
self:debug('change to fieldwork')
self.state = self.states.ON_FIELDWORK_COURSE
self.fieldworkState = self.states.WAITING_FOR_LOWER
self:startWork()
self:setDriveUnloadNow(false);
self:refreshHUD();
end
function FieldworkAIDriver:changeToUnloadOrRefill()
self:debug('changing to unload/refill course (%d waypoints)', self.unloadRefillCourse:getNumberOfWaypoints())
self:stopWork()
self:foldImplements()
self:enableCollisionDetection()
self.state = self.states.ON_UNLOAD_OR_REFILL_COURSE
end
function FieldworkAIDriver:onNextCourse()
if self.state == self.states.ON_FIELDWORK_COURSE then
if self.fieldworkState == self.states.TURNING then
-- a turn just ended, at this point all implements should already be lowered as the turn end course ended
-- but just in case, lower them now
self:lowerImplements()
self.fieldworkState = self.states.WORKING
else
self:changeToFieldwork()
end
end
end
function FieldworkAIDriver:onEndCourse()
if self.state == self.states.ON_UNLOAD_OR_REFILL_COURSE or
self.state == self.states.ON_UNLOAD_OR_REFILL_WITH_AUTODRIVE then
-- unload/refill course ended, return to fieldwork
self:debug('AI driver in mode %d continue fieldwork at %d/%d waypoints', self:getMode(), self.aiDriverData.continueFieldworkAtWaypoint, self.fieldworkCourse:getNumberOfWaypoints())
self:startFieldworkWithPathfinding(self.aiDriverData.continueFieldworkAtWaypoint)
elseif self.state == self.states.RETURNING_TO_FIRST_POINT then
AIDriver.onEndCourse(self)
else
self:debug('Fieldwork AI driver in mode %d ending fieldwork course', self:getMode())
if self:shouldReturnToFirstPoint() then
self:debug('Returning to first point')
local x, _, z = self.fieldworkCourse:getWaypointPosition(1)
if self:driveToPointWithPathfinding(x, z) then
-- pathfinding was successful, drive back to first point
self.state = self.states.RETURNING_TO_FIRST_POINT
self:raiseImplements()
self:foldImplements()
else
-- no path or too short, stop here.
AIDriver.onEndCourse(self)
-- (onEndCourse calls stopWork which raises the implements, fold must be called after that)
-- TODO: add an option to enable fold implement on work end and make it part of stopWork()
self:foldImplements()
end
else
AIDriver.onEndCourse(self)
self:foldImplements()
end
end
end
function FieldworkAIDriver:onWaypointPassed(ix)
self:debug('onWaypointPassed %d', ix)
if self.state == self.states.ON_FIELDWORK_COURSE then
if self.fieldworkState == self.states.WORKING then
-- check for transition to connecting track
if self.course:isOnConnectingTrack(ix) then
-- reached a connecting track (done with the headland, move to the up/down row or vice versa),
-- raise all implements while moving
self:debug('on a connecting track now, raising implements.')
self:raiseImplements()
self.fieldworkState = self.states.ON_CONNECTING_TRACK
end
end
if self.fieldworkState ~= self.states.TEMPORARY and self.course:isOnConnectingTrack(ix) then
-- passed a connecting track waypoint
-- check transition from connecting track to the up/down rows
-- we are close to the end of the connecting track, transition back to the up/down rows with
-- an alignment course
local d, firstUpDownWpIx = self.course:getDistanceToFirstUpDownRowWaypoint(ix)
self:debug('up/down rows start in %s meters.', tostring(d))
if d < self.vehicle.cp.turnDiameter * 2 and firstUpDownWpIx then
self:debug('End connecting track, start working on up/down rows (waypoint %d) with alignment course if needed.', firstUpDownWpIx)
self:startFieldworkWithAlignment(firstUpDownWpIx)
end
end
end
--- Check if we are at the last waypoint and should we continue with first waypoint of the course
-- or stop.
if ix == self.course:getNumberOfWaypoints() then
self:onLastWaypoint()
end
end
function FieldworkAIDriver:onWaypointChange(ix)
self:debug('onWaypointChange %d, connecting: %s, temp: %s',
ix, tostring(self.course:isOnConnectingTrack(ix)), tostring(self.states == self.states.TEMPORARY))
if self.state == self.states.ON_FIELDWORK_COURSE then
self:updateRemainingTime(ix)
self:calculateTightTurnOffset()
self:debug('Tight turn offset = %.1f', self.tightTurnOffset)
self.aiDriverOffsetZ = 0
if self.fieldworkState == self.states.ON_CONNECTING_TRACK then
if not self.course:isOnConnectingTrack(ix) then
-- reached the end of the connecting track, back to work
self:debug('connecting track ended, back to work, first lowering implements.')
self:changeToFieldwork()
end
end
if self.fieldworkState == self.states.TEMPORARY then
-- band aid to make sure we have our implements lowered by the time we end the
-- temporary course
-- TODO: fix this and also PlowAIDriver:startWork()
if ix == self.course:getNumberOfWaypoints() then
self:debug('temporary (alignment) course is about to end, start work')
self:startWork()
end
-- towards the end of the field course make sure the implement reaches the last waypoint
-- TODO: this needs refactoring, for now don't do this for temporary courses like a turn as it messes up reversing
elseif ix > self.course:getNumberOfWaypoints() - 3 and not self.course:isTemporary() then
if self.frontMarkerDistance then
self:debug('adding offset (%.1f front marker) to make sure we do not miss anything when the course ends', self.frontMarkerDistance)
self.aiDriverOffsetZ = -self.frontMarkerDistance
end
end
if self.fieldworkState ~= self.states.TURNING and self.course:isTurnStartAtIx(ix) then
self:startTurn(ix)
end
end
-- update the legacy waypoint counter on the HUD
if self.state == self.states.ON_FIELDWORK_COURSE or self.states.ON_UNLOAD_OR_REFILL_COURSE then
courseplay:setWaypointIndex(self.vehicle, self.ppc:getCurrentOriginalWaypointIx())
end
end
function FieldworkAIDriver:onTowedImplementPassedWaypoint(ix)
self:debug('Implement passsed waypoint %d', ix)
end
--- Should we return to the first point of the course after we are done?
function FieldworkAIDriver:shouldReturnToFirstPoint()
-- TODO: implement and check setting in course or HUD
if self.vehicle.cp.settings.returnToFirstPoint:is(true) then
self:debug('Returning to first point.')
return true
else
self:debug('Not returning to first point.')
return false
end
end
--- Speed on the field when not working
function FieldworkAIDriver:getFieldSpeed()
return self.vehicle.cp.speeds.field
end
-- Speed on the field when working
function FieldworkAIDriver:getWorkSpeed()
-- use the speed limit supplied by Giants for fieldwork
local speedLimit = self.vehicle:getSpeedLimit() or math.huge
return math.min(self.vehicle.cp.speeds.field, speedLimit)
end
--- Pass on self.speed set elsewhere to the AIDriver.
function FieldworkAIDriver:getSpeed()
local speed = AIDriver.getSpeed(self)
-- as long as other CP components mess with the cruise control we need to reset this, for example after
-- a turn
self.vehicle:setCruiseControlMaxSpeed(speed)
return speed
end
--- Start the actual work. Lower and turn on implements
function FieldworkAIDriver:startWork()
self:debug('Starting work: turn on and lower implements.')
-- send the event first and _then_ lower otherwise it sometimes does not turn it on
self.vehicle:raiseAIEvent("onAIStart", "onAIImplementStart")
self.vehicle:requestActionEventUpdate()
self:startEngineIfNeeded()
self:lowerImplements(self.vehicle)
end
--- Stop working. Raise and stop implements
function FieldworkAIDriver:stopWork()
self:debug('Ending work: turn off and raise implements.')
self:raiseImplements()
self.vehicle:raiseAIEvent("onAIEnd", "onAIImplementEnd")
self.vehicle:requestActionEventUpdate()
self:clearRemainingTime()
end
--- Check if need to refill/unload anything
function FieldworkAIDriver:allFillLevelsOk()
if not self.vehicle.cp.workTools then return false end
-- what here comes is basically what Giants' getFillLevelInformation() does but this returns the real fillType,
-- not the fillTypeToDisplay as this latter is different for each type of seed
local fillLevelInfo = {}
self:getAllFillLevels(self.vehicle, fillLevelInfo)
return self:areFillLevelsOk(fillLevelInfo)
end
function FieldworkAIDriver:getAllFillLevels(object, fillLevelInfo)
-- get own fill levels
if object.getFillUnits then
for _, fillUnit in pairs(object:getFillUnits()) do
local fillType = self:getFillTypeFromFillUnit(fillUnit)
local fillTypeName = g_fillTypeManager:getFillTypeNameByIndex(fillType)
self:debugSparse('%s: Fill levels: %s: %.1f/%.1f', object:getName(), fillTypeName, fillUnit.fillLevel, fillUnit.capacity)
if not fillLevelInfo[fillType] then fillLevelInfo[fillType] = {fillLevel=0, capacity=0} end
fillLevelInfo[fillType].fillLevel = fillLevelInfo[fillType].fillLevel + fillUnit.fillLevel
fillLevelInfo[fillType].capacity = fillLevelInfo[fillType].capacity + fillUnit.capacity
end
end
-- collect fill levels from all attached implements recursively
for _,impl in pairs(object:getAttachedImplements()) do
self:getAllFillLevels(impl.object, fillLevelInfo)
end
end
function FieldworkAIDriver:getFillTypeFromFillUnit(fillUnit)
local fillType = fillUnit.lastValidFillType or fillUnit.fillType
-- TODO: do we need to check more supported fill types? This will probably cover 99.9% of the cases
if fillType == FillType.UNKNOWN then
-- just get the first valid supported fill type
for ft, valid in pairs(fillUnit.supportedFillTypes) do
if valid then return ft end
end
else
return fillType
end
end
-- is the fill level ok to continue?
function FieldworkAIDriver:areFillLevelsOk()
-- implement specifics in the derived classes
return true
end
--- Set up the main (fieldwork) course and the unload/refill course and initial state
-- Currently, the legacy CP code just dumps all loaded courses to vehicle.Waypoints so
-- now we have to figure out which of that is the actual fieldwork course and which is the
-- refill/unload part.
-- This should better be handled by the course management though and should be refactored.
function FieldworkAIDriver:setUpCourses()
local nWaits = 0
local endFieldCourseIx = 0
for i, wp in ipairs(self.vehicle.Waypoints) do
if wp.wait then
nWaits = nWaits + 1
-- the second wp with the wait attribute is the end of the field course (assuming
-- the field course has been loaded first.
if nWaits == 2 then
endFieldCourseIx = i
break
end
end
end
if #self.vehicle.Waypoints > endFieldCourseIx and endFieldCourseIx ~= 0 then
self:debug('Course with %d waypoints set up, there seems to be an unload/refill course starting at waypoint %d',
#self.vehicle.Waypoints, endFieldCourseIx + 1)
---@type Course
self.fieldworkCourse = Course(self.vehicle, self.vehicle.Waypoints, false, 1, endFieldCourseIx)
---@type Course
if #self.vehicle.Waypoints - endFieldCourseIx > 2 then
self.unloadRefillCourse = Course(self.vehicle, self.vehicle.Waypoints, false, endFieldCourseIx + 1, #self.vehicle.Waypoints)
else
self:debug('Unload/refill course too short, ignoring')
end
else
self:debug('Course with %d waypoints set up, there seems to be no unload/refill course', #self.vehicle.Waypoints)
self.fieldworkCourse = Course(self.vehicle, self.vehicle.Waypoints, false, 1, #self.vehicle.Waypoints)
end
-- apply the current offset to the fieldwork part (lane+tool, where, confusingly, totalOffsetX contains the toolOffsetX)
self.fieldworkCourse:setOffset(self.vehicle.cp.totalOffsetX, self.vehicle.cp.toolOffsetZ)
end
function FieldworkAIDriver:setRidgeMarkers()
if not self.vehicle.cp.ridgeMarkersAutomatic then return end
local active = self.state == self.states.ON_FIELDWORK_COURSE and self.fieldworkState ~= self.states.TURNING
for _, workTool in ipairs(self.vehicle.cp.workTools) do
-- yes, another Giants typo. And not sure why implements with no ridge markers even have the spec_ridgeMarker
if workTool.spec_ridgeMarker and workTool.spec_ridgeMarker.numRigdeMarkers > 0 then
local state = active and self.course:getRidgeMarkerState(self.ppc:getCurrentWaypointIx()) or 0
if workTool.spec_ridgeMarker.ridgeMarkerState ~= state then
self:debug('Setting ridge markers to %d', state)
workTool:setRidgeMarkerState(state)
end
end
end
end
--- We already set the offsets on the course at start, this is to update those values
-- if the user changed them during the run or the AI driver wants to add an offset
function FieldworkAIDriver:updateFieldworkOffset()
-- (as lua passes tables by reference, we can directly change self.fieldworkCourse even if we passed self.course
-- to the PPC to drive)
self.fieldworkCourse:setOffset(self.vehicle.cp.totalOffsetX + self.aiDriverOffsetX + (self.tightTurnOffset or 0),
self.vehicle.cp.toolOffsetZ + self.aiDriverOffsetZ)
end
function FieldworkAIDriver:hasSameCourse(otherVehicle)
if otherVehicle.cp.driver and otherVehicle.cp.driver.fieldworkCourse then
return self.fieldworkCourse:equals(otherVehicle.cp.driver.fieldworkCourse)
else
return false
end
end
function FieldworkAIDriver:getCurrentFieldworkWaypointIx()
return self.fieldworkCourse:getCurrentWaypointIx()
end
--- When working in a group (convoy), do I have to hold so I don't get too close to the
-- other vehicles in front of me?
function FieldworkAIDriver:manageConvoy()
if not self.vehicle.cp.convoyActive then return false end
--get my position in convoy and look for the closest combine
local position = 1
local total = 1
local closestDistance = math.huge
for _, otherVehicle in pairs(CpManager.activeCoursePlayers) do
if otherVehicle ~= self.vehicle and otherVehicle.cp.convoyActive and self:hasSameCourse(otherVehicle) then
local myWpIndex = self:getCurrentFieldworkWaypointIx()
local otherVehicleWpIndex = otherVehicle.cp.driver:getCurrentFieldworkWaypointIx()
total = total + 1
if myWpIndex < otherVehicleWpIndex then
position = position + 1
local distance = (otherVehicleWpIndex - myWpIndex) * courseGenerator.waypointDistance
if distance < closestDistance then
closestDistance = distance
end
end
end
end
-- stop when I'm too close to the combine in front of me
if position > 1 then
if closestDistance < self.vehicle.cp.convoy.minDistance then
self:debugSparse('too close (%.1f) to other vehicles in group, holding.', closestDistance)
self:setSpeed(0)
self:overrideAutoEngineStop()
end
else
closestDistance = 0
end
-- TODO: check for change should be handled by setCpVar()
if self.vehicle.cp.convoy.distance ~= closestDistance then
self.vehicle:setCpVar('convoy.distance',closestDistance)
end
if self.vehicle.cp.convoy.number ~= position then
self.vehicle:setCpVar('convoy.number',position)
end
if self.vehicle.cp.convoy.members ~= total then
self.vehicle:setCpVar('convoy.members',total)
end
end
-- Although raising the AI start/stop events supposed to fold/unfold the implements, it does not always happen.
-- So use these to explicitly do so
function FieldworkAIDriver:unfoldImplements()
for _,workTool in pairs(self.vehicle.cp.workTools) do
if courseplay:isFoldable(workTool) then
local isFolding, isFolded, isUnfolded = courseplay:isFolding(workTool)
if not isUnfolded and workTool:getIsFoldAllowed(workTool.cp.realUnfoldDirection) then
self:debug('Unfolding %s', workTool:getName())
workTool:setFoldDirection(workTool.cp.realUnfoldDirection)
end
end
end
end
function FieldworkAIDriver:foldImplements()
for _,workTool in pairs(self.vehicle.cp.workTools) do
if courseplay:isFoldable(workTool) then
local isFolding, isFolded, isUnfolded = courseplay:isFolding(workTool)
if not isFolded and workTool:getIsFoldAllowed(-workTool.cp.realUnfoldDirection) then
self:debug('Folding %s', workTool:getName())
workTool:setFoldDirection(-workTool.cp.realUnfoldDirection)
end
end
end
end
function FieldworkAIDriver:isAllUnfolded()
for _,workTool in pairs(self.vehicle.cp.workTools) do
if courseplay:isFoldable(workTool) then
local isFolding, isFolded, isUnfolded = courseplay:isFolding(workTool)
if not isUnfolded then return false end
end
end
return true
end
function FieldworkAIDriver:clearRemainingTime()
self.vehicle.cp.timeRemaining = nil
end
function FieldworkAIDriver:updateRemainingTime(ix)
if self.state == self.states.ON_FIELDWORK_COURSE then
local dist, turns = self.course:getRemainingDistanceAndTurnsFrom(ix)
local turnTime = turns * self.turnDurationMs / 1000
self.vehicle.cp.timeRemaining = math.max(0, dist / (self:getWorkSpeed() / 3.6) + turnTime)
self:debug('Distance to go: %.1f; Turns left: %d; Time left: %ds', dist, turns, self.vehicle.cp.timeRemaining)
else
self:clearRemainingTime()
end
end
function FieldworkAIDriver:measureTurnTime()
local isTurning = self.state == self.states.ON_FIELDWORK_COURSE and self.fieldworkState == self.states.TURNING
if self.wasTurning and not isTurning then
-- end of turn
if self.turnStartedAt then
-- use sliding average to smooth jumps
self.turnDurationMs = (self.turnDurationMs + self.vehicle.timer - self.turnStartedAt) / 2
self.realTurnDurationMs = self.vehicle.timer - self.turnStartedAt
self:debug('Measured turn duration is %.0f ms', self.turnDurationMs)
end
elseif not self.wasTurning and isTurning then
-- start of turn
self.turnStartedAt = self.vehicle.timer
end
self.wasTurning = isTurning
end
function FieldworkAIDriver:checkWeather()
if self.vehicle.getIsThreshingAllowed and not self.vehicle:getIsThreshingAllowed() then
self:debugSparse('No threshing in rain...')
self:setSpeed(0)
self:setInfoText('WEATHER')
else
self:clearInfoText('WEATHER')
end
end
function FieldworkAIDriver:updateLights()
if not self.vehicle.spec_lights then return end
-- turn on beacon lights on unload/refill course when enabled
if self.state == self.states.ON_UNLOAD_OR_REFILL_COURSE and self:areBeaconLightsEnabled() then
self.vehicle:setBeaconLightsVisibility(true)
else
self:updateLightsOnField()
end
end
function FieldworkAIDriver:updateLightsOnField()
-- there are no beacons used on the field by default
self.vehicle:setBeaconLightsVisibility(false)
end
function FieldworkAIDriver:startLoweringDurationTimer()
-- then start but only after everything is unfolded as we don't want to include the
-- unfold duration (since we don't fold at the end of the row).
if self:isAllUnfolded() then
self.startedLoweringAt = self.vehicle.timer
end
end
function FieldworkAIDriver:calculateLoweringDuration()
if self.startedLoweringAt then
self.loweringDurationMs = self.vehicle.timer - self.startedLoweringAt
self:debug('Measured implement lowering duration is %.0f ms', self.loweringDurationMs)
self.startedLoweringAt = nil
end
end
function FieldworkAIDriver:getLoweringDurationMs()
return self.loweringDurationMs
end
--- If we are towing an implement, move to a bigger radius in tight turns
-- making sure that the towed implement's trajectory remains closer to the
-- course.
function FieldworkAIDriver:calculateTightTurnOffset()
local function smoothOffset(offset)
self.tightTurnOffset = (offset + 3 * (self.tightTurnOffset or 0 )) / 4
return self.tightTurnOffset
end
-- first of all, does the current waypoint have radius data?
local r = self.course:getRadiusAtIx(self.ppc:getCurrentWaypointIx())
if not r then
return smoothOffset(0)
end
local towBarLength = self:getTowBarLength()
-- Is this really a tight turn? It is when the tow bar is longer than radius / 3, otherwise
-- we ignore it.
if towBarLength < r / 3 then
return smoothOffset(0)
end
-- Ok, looks like a tight turn, so we need to move a bit left or right of the course
-- to keep the tool on the course.
local offset = self:getOffsetForTowBarLength(r, towBarLength)
if offset ~= offset then
-- check for nan
return smoothOffset(0)
end
-- figure out left or right now?
local nextAngle = self.course:getWaypointAngleDeg(self.ppc:getCurrentWaypointIx() + 1)
local currentAngle = self.course:getWaypointAngleDeg(self.ppc:getCurrentWaypointIx())
if not nextAngle or not currentAngle then
return smoothOffset(0)
end
if getDeltaAngle(math.rad(nextAngle), math.rad(currentAngle)) > 0 then offset = -offset end
-- smooth the offset a bit to avoid sudden changes
smoothOffset(offset)
self:debug('Tight turn, r = %.1f, tow bar = %.1f m, currentAngle = %.0f, nextAngle = %.0f, offset = %.1f, smoothOffset = %.1f', r, towBarLength, currentAngle, nextAngle, offset, self.tightTurnOffset )
-- remember the last value for smoothing
return self.tightTurnOffset
end
function FieldworkAIDriver:getTowBarLength()
-- is there a wheeled implement behind the tractor and is it on a pivot?
local workTool = courseplay:getFirstReversingWheeledWorkTool(self.vehicle)
if not workTool or not workTool.cp.realTurningNode then
return 0
end
-- get the distance between the tractor and the towed implement's turn node
-- (not quite accurate when the angle between the tractor and the tool is high)
local tractorX, _, tractorZ = getWorldTranslation( self:getDirectionNode() )
local toolX, _, toolZ = getWorldTranslation( workTool.cp.realTurningNode )
local towBarLength = courseplay:distance( tractorX, tractorZ, toolX, toolZ )
return towBarLength
end
function FieldworkAIDriver:getOffsetForTowBarLength(r, towBarLength)
local rTractor = math.sqrt( r * r + towBarLength * towBarLength ) -- the radius the tractor should be on
return rTractor - r
end
function FieldworkAIDriver:getFillLevelInfoText()
return 'NEEDS_REFILLING'
end
function FieldworkAIDriver:lowerImplements()
for _, implement in pairs(self.vehicle:getAttachedAIImplements()) do
implement.object:aiImplementStartLine()
end
self.vehicle:raiseStateChange(Vehicle.STATE_CHANGE_AI_START_LINE)
if FieldworkAIDriver.hasImplementWithSpecialization(self.vehicle, SowingMachine) or self.ppc:isReversing() then
-- sowing machines want to stop while the implement is being lowered
-- also, when reversing, we assume that we'll switch to forward, so stop while lowering, then start forward
self.fieldworkState = self.states.WAITING_FOR_LOWER_DELAYED
end
end
function FieldworkAIDriver:raiseImplements()
for _, implement in pairs(self.vehicle:getAttachedAIImplements()) do
implement.object:aiImplementEndLine()
end
self.vehicle:raiseStateChange(Vehicle.STATE_CHANGE_AI_END_LINE)
end
function FieldworkAIDriver:rememberWaypointToContinueFieldwork()
local bestKnownCurrentWpIx = self.ppc:getLastPassedWaypointIx() or self.ppc:getCurrentWaypointIx()
-- after we return from a refill/unload, continue a bit before the point where we left to
-- make sure not leaving any unworked patches
self.aiDriverData.continueFieldworkAtWaypoint = self.course:getPreviousWaypointIxWithinDistance(bestKnownCurrentWpIx, 10)
if self.aiDriverData.continueFieldworkAtWaypoint then
-- anything other than a turn start wp will work fine
if self.course:isTurnStartAtIx(self.aiDriverData.continueFieldworkAtWaypoint) then
self.aiDriverData.continueFieldworkAtWaypoint = self.aiDriverData.continueFieldworkAtWaypoint - 1
end
else
self.aiDriverData.continueFieldworkAtWaypoint = bestKnownCurrentWpIx
end
self:debug('Will return to fieldwork at waypoint %d', self.aiDriverData.continueFieldworkAtWaypoint)
end
function FieldworkAIDriver:getCanShowDriveOnButton()
return self.state == self.states.ON_FIELDWORK_COURSE
end
function FieldworkAIDriver:getLoweringDurationMs()
return self.loweringDurationMs
end
function FieldworkAIDriver:findLoweringDurationMs()
local function getLoweringDurationMs(object)
if object.spec_animatedVehicle then
-- TODO: implement these in the specifications?
return math.max(object.spec_animatedVehicle:getAnimationDuration('lowerAnimation'),
object.spec_animatedVehicle:getAnimationDuration('rotatePickup'))
else
return 0
end
end
self.loweringDurationMs = getLoweringDurationMs(self.vehicle)
self:debug('Lowering duration: %d ms', self.loweringDurationMs)
-- check all implements first
local implements = self.vehicle:getAttachedImplements()
for _, implement in ipairs(implements) do
local implementLoweringDurationMs = getLoweringDurationMs(implement.object)
self:debug('Lowering duration (%s): %d ms', implement.object:getName(), implementLoweringDurationMs)
if implementLoweringDurationMs > self.loweringDurationMs then
self.loweringDurationMs = implementLoweringDurationMs
end
local jointDescIndex = implement.jointDescIndex
-- now check the attacher joints
if self.vehicle.spec_attacherJoints and jointDescIndex then
local ajs = self.vehicle.spec_attacherJoints:getAttacherJoints()
local ajLoweringDurationMs = ajs[jointDescIndex] and ajs[jointDescIndex].moveDefaultTime or 0
self:debug('Lowering duration (%s attacher joint): %d ms', implement.object:getName(), ajLoweringDurationMs)
if ajLoweringDurationMs > self.loweringDurationMs then
self.loweringDurationMs = ajLoweringDurationMs
end
end
end
if not self.loweringDurationMs or self.loweringDurationMs <= 1 then
self.loweringDurationMs = 2000
self:debug('No lowering duration found, setting to: %d ms', self.loweringDurationMs)
end
self:debug('Final lowering duration: %d ms', self.loweringDurationMs)
end
--- Never continue automatically at a wait point
function FieldworkAIDriver:isAutoContinueAtWaitPointEnabled()
return false
end
-- instantiate generic turn course, derived classes may override
function FieldworkAIDriver:createTurnCourse()
return CourseTurn(self.vehicle, self, self.turnContext)
end
function FieldworkAIDriver:startTurn(ix)
-- set a short lookahead distance for PPC to increase accuracy, especially after switching back from
-- turn.lua. That often happens too early (when lowering the implement) when we still have a cross track error,
-- this should help returning to the course faster.
self.ppc:setShortLookaheadDistance()
self:setMarkers()
self.turnContext = TurnContext(self.course, ix, self.aiDriverData, self.vehicle.cp.workWidth, self.frontMarkerDistance)