feat(marker): halm efter høst — hakkes, samles op til silo eller presses til baller + ballesamler (CP BALE_FINDER); græsvalg også ved græs-mål

This commit is contained in:
2026-09-26 01:26:35 +02:00
parent af71548b0b
commit 293d66156f
10 changed files with 198 additions and 16 deletions
+57 -1
View File
@@ -5,7 +5,7 @@
ADCpBridge = {}
ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon", "spec_combine",
ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon", "spec_combine", "spec_baler",
"spec_sowingMachine", "spec_cultivator", "spec_plow", "spec_roller", "spec_weeder", "spec_mulcher"}
-- Sprøjtens/sprederens fyld -> rolle (tom sprøjte har ingen rolle; den kan ikke sendes ud).
@@ -86,6 +86,10 @@ function ADCpBridge.getSpecs(vehicle)
local ok, can = pcall(vehicle.getCanStartCpCombineUnloader, vehicle)
specs.canUnload = ok and can == true or nil
end
if vehicle.getCanStartCpBaleFinder ~= nil then
local ok, can = pcall(vehicle.getCanStartCpBaleFinder, vehicle)
specs.canBaleCollect = ok and can == true or nil
end
return specs
end
@@ -189,6 +193,58 @@ function ADCpBridge.getRefillLevel(vehicle, fillType)
return total
end
-- Halmskår bag mejetærskeren (Courseplays køretøjsindstilling strawSwath: 1 = fra/hakkes, 2 = til).
function ADCpBridge.setStrawSwath(vehicle, on)
return (pcall(function() vehicle:getCpSettings().strawSwath:setValue(on and 2 or 1) end))
end
-- Ballesamler: Courseplays BALE_FINDER_CP (ballevogn eller Universal Autoload) samler markens baller.
function ADCpBridge.startBaleFinder(vehicle, fieldX, fieldZ, onDone)
if AIJobType == nil or AIJobType.BALE_FINDER_CP == nil then
return nil, "Courseplay (BALE_FINDER_CP) findes ikke"
end
local farmId = vehicle:getOwnerFarmId()
local job = g_currentMission.aiJobTypeManager:createJob(AIJobType.BALE_FINDER_CP)
if job == nil then return nil, "kunne ikke oprette ballesamler-job" end
pcall(function() job:applyCurrentState(vehicle, g_currentMission, farmId, false, false) end)
local params = job.cpJobParameters
if params ~= nil and params.fieldPosition ~= nil then pcall(function() params.fieldPosition:setPosition(fieldX, fieldZ) end) end
if params ~= nil and params.startPosition ~= nil then
local vx, _, vz = getWorldTranslation(vehicle.rootNode)
pcall(function() params.startPosition:setPosition(vx, vz) end)
end
pcall(function() job:setValues() end)
local handle = {cancelled = false, fired = false, finished = false}
local function finish(success, reason)
if handle.finished then return end
handle.finished = true
if not handle.cancelled then onDone(success, reason) end
end
local function startJob()
if handle.cancelled then return end
local valid, err = job:validate(farmId)
if not valid then return finish(false, "Courseplay: " .. tostring(err)) end
g_currentMission.aiSystem:startJob(job, farmId)
finish(true)
end
handle.onBoundary = function(_, success, message)
if handle.fired then return end
handle.fired = true
if not success then return finish(false, "Courseplay fandt ikke markgrænsen: " .. tostring(message)) end
startJob()
end
if job.registerFieldBoundaryDetectionCallback ~= nil then
job:registerFieldBoundaryDetectionCallback(handle, handle.onBoundary)
end
local isValid, isRunning, message = job:detectFieldBoundary()
if isValid and not isRunning then
startJob()
elseif not isRunning then
return nil, "Courseplay: " .. tostring(message)
end
return handle
end
-- Stop køretøjets AI-job (Courseplay/hjælper).
function ADCpBridge.stop(vehicle)
pcall(function()
+2
View File
@@ -8,6 +8,7 @@ ADFieldFleet = {}
-- plov, ikke aflæsningsvogn). sprayRole kommer fra sprøjtens/sprederens fyld (gødning/kalk/ukrudt).
ADFieldFleet.ROLE_SPECS = {
{role = "harvester", spec = "spec_combine"},
{role = "baler", spec = "spec_baler"},
{role = "collector", spec = "spec_forageWagon"},
{role = "rake", spec = "spec_windrower"},
{role = "tedder", spec = "spec_tedder"},
@@ -19,6 +20,7 @@ ADFieldFleet.ROLE_SPECS = {
{role = "mulcher", spec = "spec_mulcher"},
{role = "cultivator", spec = "spec_cultivator"},
{role = "plow", spec = "spec_plow"},
{role = "baleCollector", spec = "canBaleCollect"},
{role = "unloader", spec = "canUnload"},
}
+18 -7
View File
@@ -28,7 +28,7 @@ ADFieldModel.ROLES = {
mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector",
harvesting = "harvester", mulching = "mulcher", liming = "limer", plowing = "plow",
cultivating = "cultivator", sowing = "seeder", rolling = "roller", weeding = "weeder",
fertilizing = "fertilizer",
fertilizing = "fertilizer", strawCollecting = "collector", baling = "baler", baleCollecting = "baleCollector",
}
ADFieldModel.HELPERS = {harvesting = "unloader"}
ADFieldModel.CARE_STAGES = {fertilizing = true, weeding = true, careLiming = true}
@@ -42,8 +42,11 @@ KNOWN_STAGES.careLiming = true
local DEFAULT_PLAN = {
mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1,
mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false,
fertilize = true, weed = true, targetFruit = "", prepAfterMow = false,
fertilize = true, weed = true, targetFruit = "", prepAfterMow = false, straw = "chop",
}
local STRAW_VALUES = {chop = true, collect = true, bale = true}
-- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges)
local STRAW_STAGES = {collect = {"strawCollecting"}, bale = {"baling", "baleCollecting"}}
local SOIL_VALUES = {auto = true, cultivate = true, plow = true, none = true}
local PRODUCTS = {auto = true, silage = true, hay = true, harvest = true}
@@ -116,6 +119,7 @@ function ADFieldModel.workStages(plan, kind, needs)
local product = ADFieldModel.product(plan, kind)
if product == "harvest" then
local stages = {"harvesting"}
for _, stage in ipairs(STRAW_STAGES[plan.straw or "chop"] or {}) do table.insert(stages, stage) end
for _, stage in ipairs(ADFieldModel.prepStages(plan, needs)) do table.insert(stages, stage) end
return stages
end
@@ -168,9 +172,10 @@ function ADFieldModel.shouldStartBare(field, kind, plantableNow)
return plantableNow == true
end
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner.
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner (halmskår ligger i
-- høsterens spor — der er breddekravet ligegyldigt).
function ADFieldModel.requiredWidth(field, role)
if role == "collector" and not field.plan.rake then
if role == "collector" and not field.plan.rake and field.stage ~= "strawCollecting" then
return field.mowWidth
end
return nil
@@ -178,6 +183,8 @@ end
-- Hvilket tidligere kursus sættet skal køre: læssevognen følger skårene; de andre laver deres eget.
function ADFieldModel.courseSource(field, role)
-- halm: læssevogn og presser kører høsterens kursus (skåret ligger i mejetærskerens spor)
if field.stage == "strawCollecting" or role == "baler" then return "harvester" end
if role ~= "collector" then return nil end
return field.plan.rake and "rake" or "mower"
end
@@ -247,6 +254,7 @@ local function freshCycle(copy)
copy.mowWidth = nil
copy.mowVehicleId = nil
copy.rakeVehicleId = nil
copy.harvestVehicleId = nil
copy.windrowOffset = nil
copy.careRuns = {}
end
@@ -276,7 +284,7 @@ function ADFieldModel.advance(field, needs)
local stages = ADFieldModel.workStages(field.plan, field.kind, needs)
copy.stage = "done"
local passed = false
local order = {"mowing", "tedding", "raking", "collecting", "harvesting", "mulching", "liming", "plowing", "cultivating", "sowing", "rolling"}
local order = {"mowing", "tedding", "raking", "collecting", "harvesting", "strawCollecting", "baling", "baleCollecting", "mulching", "liming", "plowing", "cultivating", "sowing", "rolling"}
local rank = {}
for index, stage in ipairs(order) do rank[stage] = index end
for _, stage in ipairs(stages) do
@@ -303,7 +311,7 @@ end
-- Vejrfølsomme trin: afgrøde og skår skal være tørre (høst, slåning, vending, rivning, hø-opsamling).
-- Jordbehandling, såning, pleje og ensilage-opsamling kan køre i regn.
local WEATHER_STAGES = {harvesting = true, mowing = true, tedding = true, raking = true}
local WEATHER_STAGES = {harvesting = true, mowing = true, tedding = true, raking = true, strawCollecting = true, baling = true}
function ADFieldModel.isWeatherSensitive(stage, plan, kind)
if WEATHER_STAGES[stage] then return true end
return stage == "collecting" and ADFieldModel.product(plan, kind or "mowable") == "hay"
@@ -423,11 +431,12 @@ function ADFieldModel.toRecord(field)
rigs = plan.rigs, unloaders = plan.unloaders, mulch = flag(plan.mulch), lime = flag(plan.lime),
soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll),
fertilize = flag(plan.fertilize), weed = flag(plan.weed),
targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow),
targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop",
kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "),
mowWidth = field.mowWidth or 0,
mowVehicle = field.mowVehicleId or "",
rakeVehicle = field.rakeVehicleId or "",
harvestVehicle = field.harvestVehicleId or "",
windrowOffset = field.windrowOffset ~= nil and string.format("%.2f", field.windrowOffset) or "",
}
end
@@ -462,12 +471,14 @@ function ADFieldModel.fromRecord(record)
plan.weed = readFlag(record.weed, true)
plan.targetFruit = record.targetFruit or ""
plan.prepAfterMow = readFlag(record.prepAfterMow, false)
plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop"
field.kind = readText(record.kind)
field.lastFruit = readText(record.lastFruit)
for stage, runs in string.gmatch(record.care or "", "(%w+)=(%d+)") do field.careRuns[stage] = tonumber(runs) end
field.mowWidth = (record.mowWidth or 0) > 0 and record.mowWidth or nil
field.mowVehicleId = readText(record.mowVehicle)
field.rakeVehicleId = readText(record.rakeVehicle)
field.harvestVehicleId = readText(record.harvestVehicle)
field.windrowOffset = tonumber(record.windrowOffset)
return field
end
+2 -2
View File
@@ -11,8 +11,8 @@ ADFieldStorage.FIELD_TYPES = {
id = "Int", stage = "String", mode = "String", product = "String", rake = "Int", store = "String",
rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int",
sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int",
kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int",
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", windrowOffset = "String",
kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int", straw = "String",
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", harvestVehicle = "String", windrowOffset = "String",
}
function ADFieldStorage.getPath()
+59 -2
View File
@@ -166,6 +166,9 @@ end
-- Hvilken vare markens trin fører til lager: skår (ensilage), tørskår (hø) eller høstet afgrøde.
function ADFieldWork.storeFillType(field, info)
if field.stage == "harvesting" then return info.harvestFillType end
if field.stage == "strawCollecting" or field.stage == "baling" or field.stage == "baleCollecting" then
return g_fillTypeManager:getFillTypeIndexByName("STRAW")
end
local product = ADFieldModel.product(field.plan, field.kind or info.kind)
return product == "hay" and info.hay or info.windrow
end
@@ -202,11 +205,11 @@ local function coursesOf(fieldId)
return ADFieldJobs.courses[fieldId]
end
-- Kurset et trin skal genbruge ("mower"/"rake"): gemt kopi, ellers maskinens nuværende kursus.
-- Kurset et trin skal genbruge ("mower"/"rake"/"harvester"): gemt kopi, ellers maskinens nuværende kursus.
local function sourceCourse(adEnv, field, source)
local saved = coursesOf(field.id)[source]
if saved ~= nil then return saved end
local vehicleId = source == "rake" and field.rakeVehicleId or field.mowVehicleId
local vehicleId = source == "rake" and field.rakeVehicleId or source == "harvester" and field.harvestVehicleId or field.mowVehicleId
if vehicleId == nil then return nil end
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
if ADRunsController.getVehicleId(vehicle) == vehicleId then return ADCpBridge.getCourse(vehicle) end
@@ -435,6 +438,51 @@ local function beginUnloader(adEnv, field, info, assignment)
assignment.handle = handle
end
-- Ballesamler: Courseplays ballefinder; fuld vogn tømmes af AutoDrive i nærmeste ballelager (palleskur der
-- tager halmballer), som læssevognen. Uden lager stopper den når den er fuld.
local function beginBaleCollector(adEnv, field, info, assignment)
local vehicle = assignment.vehicle
local farmId = vehicle:getOwnerFarmId()
local straw = g_fillTypeManager:getFillTypeIndexByName("STRAW")
local isReachable = function(markerId) return ADUnloadWait.hasRouteTo(adEnv, vehicle, markerId) end
local store = ADSources.findPalletStore(adEnv, vehicle, straw, nil, isReachable, farmId)
if store ~= nil then
pcall(function()
local module = stateModule(vehicle)
module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
module:setFirstMarkerByWayPointId(info.markerId)
module:setSecondMarkerByWayPointId(store.wayPointId)
module:setStartHelper(true)
module.usedHelper = helperCp(adEnv)
enableParkAtJobFinished(vehicle)
module:raiseDirtyFlag()
end)
assignment.adHandlesEnd = true
log(field.id, "ballesamler '%s' kører fulde læs til %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
else
log(field.id, "intet ballelager med plads og vej fundet — ballesamleren stopper når den er fuld")
end
local point = info.points[1] or {x = info.x, z = info.z}
assignment.phase = "starting"
assignment.since = now()
local handle, err = ADCpBridge.startBaleFinder(vehicle, point.x, point.z, function(ok, reason)
if ok then
assignment.phase = "working"
assignment.since = now()
log(field.id, "Courseplay startet (ballesamler)")
else
ADFieldWork.release(assignment)
fail(field.id, "ballesamler: " .. tostring(reason))
end
end)
if handle == nil then
ADFieldWork.release(assignment)
fail(field.id, "ballesamler: " .. tostring(err))
return
end
assignment.handle = handle
end
-- Multi-tool: følgere venter på lederens kursus for trinnet og kopierer det med deres bane.
local function courseOptions(adEnv, field, assignment)
local source = ADFieldModel.courseSource(field, assignment.role)
@@ -459,6 +507,12 @@ end
local function beginWork(adEnv, field, info, assignment)
local vehicle = assignment.vehicle
if assignment.role == "unloader" then return beginUnloader(adEnv, field, info, assignment) end
if assignment.role == "baleCollector" then return beginBaleCollector(adEnv, field, info, assignment) end
-- høsteren: halmskår efter planen (hakkes = fra; samles op/presses = skår)
if assignment.role == "harvester" then
local keepStraw = field.plan.straw == "collect" or field.plan.straw == "bale"
ADCpBridge.setStrawSwath(vehicle, keepStraw)
end
local opts, source = courseOptions(adEnv, field, assignment)
if opts == nil then
-- følger uden lederens kursus endnu
@@ -504,6 +558,9 @@ local function beginWork(adEnv, field, info, assignment)
elseif assignment.role == "rake" then
note(field.id, {rakeVehicleId = assignment.vehicleId})
rememberCourse(field, "rake", vehicle)
elseif assignment.role == "harvester" then
note(field.id, {harvestVehicleId = assignment.vehicleId})
rememberCourse(field, "harvester", vehicle)
end
if assignment.leader and (assignment.groupSize or 1) > 1 then
rememberCourse(field, field.stage, vehicle)
+17 -4
View File
@@ -9,7 +9,7 @@ SmartPickupFrame.FIELD_ROW_KINDS = {
fieldProduct = true, fieldMode = true, fieldRake = true, fieldStore = true, fleetVehicle = true,
fieldRigs = true, fieldUnloaders = true, fieldMulch = true, fieldLime = true, fieldSoil = true,
fieldSow = true, fieldSowFruit = true, fieldRoll = true, fieldFertilize = true, fieldWeed = true,
fieldTarget = true, fleetMode = true,
fieldTarget = true, fleetMode = true, fieldStraw = true,
}
SmartPickupFrame.ROLE_KEYS = {
mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake",
@@ -17,6 +17,7 @@ SmartPickupFrame.ROLE_KEYS = {
mulcher = "spu_field_role_mulcher", limer = "spu_field_role_limer", plow = "spu_field_role_plow",
cultivator = "spu_field_role_cultivator", seeder = "spu_field_role_seeder", roller = "spu_field_role_roller",
weeder = "spu_field_role_weeder", fertilizer = "spu_field_role_fertilizer",
baler = "spu_field_role_baler", baleCollector = "spu_field_role_baleCollector",
}
SmartPickupFrame.SOIL_VALUES = {"auto", "cultivate", "plow", "none"}
-- simple Ja/Nej-valg på planen: rækketype -> plan-felt
@@ -25,7 +26,8 @@ SmartPickupFrame.PLAN_TOGGLES = {
fieldFertilize = "fertilize", fieldWeed = "weed",
}
-- valg med flere muligheder: rækketype -> plan-felt
SmartPickupFrame.PLAN_CHOICES = {fieldRigs = "rigs", fieldUnloaders = "unloaders", fieldSoil = "soil", fieldSowFruit = "sowFruit"}
SmartPickupFrame.PLAN_CHOICES = {fieldRigs = "rigs", fieldUnloaders = "unloaders", fieldSoil = "soil", fieldSowFruit = "sowFruit", fieldStraw = "straw"}
SmartPickupFrame.STRAW_VALUES = {"chop", "collect", "bale"}
-- ---- tekster ----
@@ -200,14 +202,25 @@ function SmartPickupFrame:fillFieldDetail(adEnv, info)
end
local rigTexts, rigValues = numberTexts(1, ADFieldModel.MAX_RIGS)
self:addChoiceRow("fieldRigs", i18n:getText("spu_field_rigs"), rigTexts, rigValues, plan.rigs or 1)
if info.kind == "mowable" then
-- græs-valgene (ensilage/hø, riv) også når målet er en slåafgrøde (fx græs på en kornmark)
local targetMowable = false
if hasTarget and ADFields ~= nil and g_fruitTypeManager ~= nil then
local targetFruit = g_fruitTypeManager:getFruitTypeByName(plan.targetFruit)
targetMowable = targetFruit ~= nil and ADFields.getMowable()[targetFruit.index] ~= nil
end
if info.kind == "mowable" or targetMowable then
local isHay = ADFieldModel.product(plan, "mowable") == "hay"
self:addToggleRow("fieldProduct", i18n:getText("spu_field_plan"), {i18n:getText("spu_field_plan_silage"), i18n:getText("spu_field_plan_hay")}, isHay)
self:addToggleRow("fieldRake", i18n:getText("spu_field_rake"), yesNo, plan.rake == true)
else
end
-- klargøring: høst-/bare marker, og græsmarker der skal skifte til en anden mål-afgrøde
if info.kind ~= "mowable" or (hasTarget and info.fruitName ~= plan.targetFruit) then
if info.kind == "harvest" then
local unloaderTexts, unloaderValues = numberTexts(1, ADFieldModel.MAX_UNLOADERS)
self:addChoiceRow("fieldUnloaders", i18n:getText("spu_field_unloaders"), unloaderTexts, unloaderValues, plan.unloaders or 1)
local strawTexts = {}
for _, value in ipairs(SmartPickupFrame.STRAW_VALUES) do table.insert(strawTexts, i18n:getText("spu_field_straw_" .. value)) end
self:addChoiceRow("fieldStraw", i18n:getText("spu_field_straw"), strawTexts, SmartPickupFrame.STRAW_VALUES, plan.straw or "chop")
end
self:addTextRow(i18n:getText(info.kind == "harvest" and "spu_field_section_after" or "spu_field_section_prep"))
self:addToggleRow("fieldMulch", i18n:getText("spu_field_mulch"), yesNo, plan.mulch == true)
+9
View File
@@ -160,6 +160,15 @@
<text name="spu_field_target_switch" text="%s får lov at vokse færdig og høstes/slås; derefter klargøres og tilsås med %s"/>
<text name="spu_field_waitSowing" text="venter på såtid for %s"/>
<text name="spu_field_fleetAll" text="Brug alle ledige maskiner (Nej = kun med flueben)"/>
<text name="spu_field_role_baler" text="ballepresser"/>
<text name="spu_field_role_baleCollector" text="ballesamler"/>
<text name="spu_field_stage_strawCollecting" text="Halm samles"/>
<text name="spu_field_stage_baling" text="Presses"/>
<text name="spu_field_stage_baleCollecting" text="Baller samles"/>
<text name="spu_field_straw" text="Halm efter høst"/>
<text name="spu_field_straw_chop" text="Hakkes (spredes på marken)"/>
<text name="spu_field_straw_collect" text="Samles op til silo"/>
<text name="spu_field_straw_bale" text="Presses til baller og samles"/>
<text name="spu_button_start" text="Start run"/>
<text name="spu_button_stop" text="Stop run"/>
<text name="spu_button_new" text="Nyt run"/>
+9
View File
@@ -160,6 +160,15 @@
<text name="spu_field_target_switch" text="%s is left to finish growing and is harvested/mowed; then the field is prepared and sown with %s"/>
<text name="spu_field_waitSowing" text="waiting for sowing time for %s"/>
<text name="spu_field_fleetAll" text="Use all free machines (No = only ticked ones)"/>
<text name="spu_field_role_baler" text="baler"/>
<text name="spu_field_role_baleCollector" text="bale collector"/>
<text name="spu_field_stage_strawCollecting" text="Collecting straw"/>
<text name="spu_field_stage_baling" text="Baling"/>
<text name="spu_field_stage_baleCollecting" text="Collecting bales"/>
<text name="spu_field_straw" text="Straw after harvest"/>
<text name="spu_field_straw_chop" text="Chopped (spread on the field)"/>
<text name="spu_field_straw_collect" text="Collected to a silo"/>
<text name="spu_field_straw_bale" text="Baled and collected"/>
<text name="spu_button_start" text="Start run"/>
<text name="spu_button_stop" text="Stop run"/>
<text name="spu_button_new" text="New run"/>