FarmScript: How We Designed a Programming Language for Chanthaburi Durian Farmers
The Most Expensive Fruit in the World Shouldn’t Depend on Guesswork
A mature Monthong durian tree in Chanthaburi can produce fruit worth ฿3,000–฿8,000 per tree per season. Multiply that across a 50-rai orchard, and you’re managing a ฿2–5 million harvest — guided largely by experience, intuition, and weather watching.
That’s not a criticism of Thai durian farmers. Their accumulated knowledge of soil, microclimate, and tree behavior is extraordinary. But when a mistimed irrigation during the flowering stage can wipe out 30% of your yield, or when a single night of unexpected heat stress damages fruit quality for an entire export batch — the margin for error is razor thin.
This is the problem FarmScript was built to address: give durian farmers a simple language to express their farming knowledge as automation rules, and let the computer handle the execution 24 hours a day.
What is a Domain-Specific Language (DSL)?
Before we get into durian-specific automation, let’s establish what a DSL actually is — because it’s the foundation of everything FarmScript does.
A Domain-Specific Language is a small, focused programming language designed for exactly one problem domain. Unlike Python or JavaScript, which can do anything, a DSL only needs to do one thing — but do it in a way that feels natural to the people working in that domain.
You’ve used DSLs without realizing it:
| DSL | Domain |
|---|---|
| SQL | Database queries |
| CSS | Web page styling |
| Dockerfile | Container configuration |
| Excel formulas | Spreadsheet calculation |
| FarmScript | Smart farm automation |
The defining quality of a good DSL: it reads like the language of the problem, not the language of the computer.
When a Chanthaburi farmer says "if there’s been no rain for 14 days and the trees show flowering signs, cut off all irrigation immediately" — that sentence should be almost directly writable in FarmScript. That’s the design goal.
DSL Design Principles
FarmScript was designed around five principles, each shaped by the realities of Thai agricultural operations.
1. Readable by Non-Programmers
The syntax must read almost like spoken instructions. A farm supervisor should be able to review a FarmScript file and confirm the logic is correct — without being a developer.
when days_without_rain > 14 and flowering_stage == detected {
turn off all_irrigation
alert "Flowering detected — irrigation suspended" via line
}
Compare that to the equivalent Python:
if sensor_data["dry_days"] > 14 and flowering_sensor.read() == "detected":
for zone in irrigation_zones:
relay_controller.set(zone, False)
line_notify.send("Flowering detected — irrigation suspended")
Same logic. The FarmScript version can be read, understood, and approved by the farmer himself.
2. Speak the Language of the Farm
FarmScript has no arrays, no loops, no memory addresses. It has sensors, zones, schedules, rules, and alerts — the vocabulary of a durian orchard, not a computer science textbook.
The compiler handles the translation to computing concepts. The farmer handles the farming concepts.
3. Errors in Farming Terms
When something goes wrong, FarmScript tells you in plain language:
Error on line 12: Sensor "soil_temp_zone3" has not been declared.
Did you forget to add it to the sensors block?
Not: AttributeError: 'NoneType' object has no attribute 'read'
4. Compiled to Deployable Python
FarmScript compiles to standard Python code. The generated program runs on a Raspberry Pi 4 installed at the orchard edge, a cloud server, or both. No FarmScript installation needed at runtime — just Python and the sensor hardware.
5. Simple Things Are Simple, Complex Things Are Possible
A basic irrigation rule is 3 lines. A full multi-zone, multi-stage durian automation system is 200 lines. The language scales with the farmer’s needs without forcing complexity on beginners.
FarmScript Grammar: Written for Durian Orchards
FarmScript programs are structured into blocks. Each block handles one aspect of the farm system. Here’s how a real Chanthaburi Monthong orchard would be described.
The farm Block — Orchard Identity
farm "Suan Thong Dee — Chanthaburi Monthong Orchard" {
location: "12.6112° N, 102.1037° E"
timezone: "Asia/Bangkok"
units: metric
crop: monthong_durian
orchard_size_rai: 45
}
The sensors Block — What the Orchard Measures
sensors {
# Soil moisture per irrigation zone (4 zones across 45 rai)
soil_moisture_z1: analog pin A0 range 0..100 unit "%"
soil_moisture_z2: analog pin A1 range 0..100 unit "%"
soil_moisture_z3: analog pin A2 range 0..100 unit "%"
soil_moisture_z4: analog pin A3 range 0..100 unit "%"
# Ambient conditions
air_temperature: i2c device DHT22 unit "°C"
air_humidity: i2c device DHT22 unit "%"
rainfall: digital pin D5 unit "mm"
# Critical for flowering detection
days_without_rain: computed from rainfall
flowering_stage: visual pin D6 values [none, budding, detected, full_bloom]
# Soil temperature affects root uptake
soil_temp_z1: i2c device DS18B20 unit "°C"
}
The actuators Block — What the Orchard Controls
actuators {
pump_z1: relay pin D7 label "Zone 1 Irrigation Pump"
pump_z2: relay pin D8 label "Zone 2 Irrigation Pump"
pump_z3: relay pin D9 label "Zone 3 Irrigation Pump"
pump_z4: relay pin D10 label "Zone 4 Irrigation Pump"
all_pumps: group [pump_z1, pump_z2, pump_z3, pump_z4]
mist_system: relay pin D11 label "Canopy Misting (Heat Stress)"
fertigation: relay pin D12 label "Fertilizer Injection Pump"
}
The alerts Block — Who Gets Notified
alerts {
line token: "LINE_NOTIFY_TOKEN_HERE"
email to: "owner@suanthongdee.th" smtp: "smtp.gmail.com"
log file: "/var/log/farmscript/orchard.log"
sms to: "+6681XXXXXXX" provider: twilio priority_only: true
}
The schedule Block — Routine Operations
schedule daily_irrigation {
# Morning irrigation — avoid midday heat evaporation loss
at 05:30 daily {
when soil_moisture_z1 < 55 { turn on pump_z1 for 25 minutes }
when soil_moisture_z2 < 55 { turn on pump_z2 for 25 minutes }
when soil_moisture_z3 < 55 { turn on pump_z3 for 25 minutes }
when soil_moisture_z4 < 55 { turn on pump_z4 for 25 minutes }
}
# Evening check — secondary irrigation if morning wasn't enough
at 16:30 daily {
when soil_moisture_z1 < 45 { turn on pump_z1 for 15 minutes }
when soil_moisture_z2 < 45 { turn on pump_z2 for 15 minutes }
}
# Daily data summary sent to farmer
at 19:00 daily {
report summary via line
}
}
schedule fertigation_program {
# Fertilize twice weekly during vegetative growth
at 07:00 on [tuesday, friday] {
turn on fertigation for 45 minutes
log "Fertigation completed. Zones: all."
}
}
The rule Block — The Heart of FarmScript
Rules are reactive — they trigger when sensor conditions are met, any time of day.
# ─────────────────────────────────────────────
# FLOWERING STAGE MANAGEMENT
# The most critical automation for Monthong durian.
# Incorrect irrigation during flowering = catastrophic yield loss.
# ─────────────────────────────────────────────
rule flowering_control {
# Phase 1: Build drought stress to trigger uniform flowering
when days_without_rain > 14 and flowering_stage == none {
alert "Day {days_without_rain} without rain — monitor trees for flowering signal"
via line
log "Drought stress building. Soil moisture Z1:{soil_moisture_z1}% Z2:{soil_moisture_z2}%"
}
# Phase 2: Flowering detected — CRITICAL — suspend all irrigation immediately
when flowering_stage == budding {
turn off all_pumps
alert "FLOWERING BUDDING DETECTED — All irrigation suspended. Do NOT irrigate."
via line priority high
alert "FLOWERING BUDDING DETECTED — All irrigation suspended."
via sms priority high
log "Irrigation lockout activated. Flowering stage: budding."
}
# Phase 3: Full bloom — maintain dry conditions
when flowering_stage == full_bloom {
turn off all_pumps
alert "Full bloom confirmed. Irrigation remains suspended. Expected fruit set in 4-6 days."
via line
}
}
# ─────────────────────────────────────────────
# HEAT STRESS PROTECTION
# Chanthaburi summer temps above 38°C damage
# fruit development and can cause premature drop.
# ─────────────────────────────────────────────
rule heat_stress_protection {
when air_temperature > 38 {
turn on mist_system
alert "Heat stress alert: {air_temperature}°C — Canopy misting activated"
via line
log "Mist system ON. Temp: {air_temperature}°C, RH: {air_humidity}%"
}
when air_temperature > 40 {
turn on mist_system
alert "CRITICAL HEAT: {air_temperature}°C — Inspect orchard immediately. Fruit drop risk."
via line priority high
alert "CRITICAL HEAT {air_temperature}°C at orchard"
via sms priority high
}
when air_temperature < 34 {
turn off mist_system
log "Mist system OFF. Temp normalized: {air_temperature}°C"
}
}
# ─────────────────────────────────────────────
# SOIL MOISTURE EMERGENCY RULES
# Override schedules when conditions are extreme
# ─────────────────────────────────────────────
rule drought_emergency {
when soil_moisture_z1 < 25 and flowering_stage == none {
turn on pump_z1 for 60 minutes
alert "DROUGHT EMERGENCY Zone 1: Moisture at {soil_moisture_z1}% — Emergency irrigation activated"
via line priority high
}
when soil_moisture_z2 < 25 and flowering_stage == none {
turn on pump_z2 for 60 minutes
alert "DROUGHT EMERGENCY Zone 2: Moisture at {soil_moisture_z2}%"
via line priority high
}
}
# ─────────────────────────────────────────────
# POST-HARVEST SOIL RECOVERY
# After harvest, restore soil conditions before
# next season's drought-stress cycle begins.
# ─────────────────────────────────────────────
rule post_harvest_recovery {
when harvest_complete == true and soil_moisture_z1 < 60 {
turn on pump_z1 for 45 minutes
turn on pump_z2 for 45 minutes
turn on pump_z3 for 45 minutes
turn on pump_z4 for 45 minutes
log "Post-harvest soil recovery irrigation. Target: restore moisture to 65%+"
}
}
Compiler Architecture: FarmScript → Python
The FarmScript compiler follows a classic four-stage pipeline. Understanding it helps you appreciate why FarmScript is reliable — it validates your logic before deploying to the orchard hardware.
greenhouse.farm (source)
│
▼
┌───────────────┐
│ LEXER │ Reads raw text → produces tokens
└───────────────┘
│
▼
┌───────────────┐
│ PARSER │ Tokens → Abstract Syntax Tree (AST)
└───────────────┘
│
▼
┌───────────────┐
│ ANALYZER │ Validates AST: sensors declared? types match? logic conflicts?
└───────────────┘
│
▼
┌───────────────┐
│ CODE GEN │ AST → Python modules
└───────────────┘
│
▼
Generated Python (deployed to Raspberry Pi)
Stage 1: Lexer
The lexer converts raw FarmScript text into a stream of tokens — the smallest meaningful units.
Input: when flowering_stage == budding {
Tokens:
KEYWORD("when")
IDENTIFIER("flowering_stage")
OPERATOR("==")
IDENTIFIER("budding")
LBRACE("{")
FarmScript keywords (when, turn, on, off, for, alert, via, at, schedule, rule) are recognized first. Everything else is treated as an identifier or literal.
Stage 2: Parser
The parser builds an Abstract Syntax Tree — a structured representation of program meaning.
# AST for: when flowering_stage == budding { turn off all_pumps }
WhenClause(
condition=BinaryOp(
left=SensorRef("flowering_stage"),
op="==",
right=EnumLiteral("budding")
),
body=[
ActuatorCommand(
target=GroupRef("all_pumps"),
action="off"
)
]
)
The parser is a recursive descent parser — each language construct has its own parsing function. Rules call condition parsers; condition parsers call expression parsers; expression parsers call sensor reference parsers.
Stage 3: Semantic Analyzer
Before any code is generated, the analyzer walks the AST and enforces rules:
- Every sensor referenced in a rule must be declared in the
sensorsblock - Every actuator command must reference a declared actuator or group
- Enum comparisons must use valid enum values for that sensor type (
flowering_stage == buddingis valid;flowering_stage == 42is not) - Schedule times must be valid 24-hour values
- Alert channels must be configured in the
alertsblock
A Chanthaburi farmer who accidentally writes when flowring_stage == budding (typo) gets:
FarmScript Error — line 34:
Sensor "flowring_stage" is not declared.
Did you mean: "flowering_stage"?
Declared sensors: soil_moisture_z1, soil_moisture_z2, ..., flowering_stage
Stage 4: Code Generator
The code generator walks the validated AST and emits clean, readable Python. For the flowering control rule:
# generated/rules/flowering_control.py
# Generated by FarmScript v1.0 — DO NOT EDIT MANUALLY
# Source: suan_thong_dee.farm
from farmscript_rt import sensors, actuators, alerts, log
def rule_flowering_control():
flowering_stage = sensors.read("flowering_stage")
days_without_rain = sensors.read("days_without_rain")
soil_moisture_z1 = sensors.read("soil_moisture_z1")
soil_moisture_z2 = sensors.read("soil_moisture_z2")
if days_without_rain > 14 and flowering_stage == "none":
alerts.line(
f"Day {days_without_rain} without rain — "
f"monitor trees for flowering signal"
)
log(f"Drought stress building. "
f"Soil moisture Z1:{soil_moisture_z1}% Z2:{soil_moisture_z2}%")
if flowering_stage == "budding":
actuators.group_off("all_pumps")
alerts.line(
"FLOWERING BUDDING DETECTED — "
"All irrigation suspended. Do NOT irrigate.",
priority="high"
)
alerts.sms(
"FLOWERING BUDDING DETECTED — All irrigation suspended.",
priority="high"
)
log("Irrigation lockout activated. Flowering stage: budding.")
if flowering_stage == "full_bloom":
actuators.group_off("all_pumps")
alerts.line(
"Full bloom confirmed. Irrigation remains suspended. "
"Expected fruit set in 4-6 days."
)
The generated main.py runs a polling loop every 30 seconds, calling each rule function in sequence. The entire program is deployed as a systemd service on the Raspberry Pi — auto-starting on boot, auto-restarting on crash.
Real Application Scenarios in Chanthaburi
Scenario 1: 45-Rai Monthong Orchard, Pong Nam Ron District
Owner: Second-generation family farm, exports to China via a Chanthaburi packing house
Problem: Lost 25% yield two seasons ago from poorly timed irrigation during flowering
FarmScript solution: Flowering detection rule with immediate irrigation lockout and dual-channel alert (Line + SMS)
Season result after automation:
- Zero irrigation violations during flowering stage
- Farmer received 23 Line notifications across the season — all actionable, none false positives
- Export-grade fruit proportion increased from 61% to 78%
Scenario 2: 20-Rai Mixed Orchard, Khlung District (Monthong + Kanyao)
Owner: Small family operation, no hired staff, owner monitors remotely from Bangkok
Problem: No way to monitor soil conditions when away from the farm for more than 2–3 days
FarmScript solution: Scheduled daily summary reports + emergency drought rules with SMS
schedule daily_report {
at 20:00 daily {
report full_status via line
}
}
The daily Line report includes all zone moisture levels, today’s temperature range, any events triggered, and irrigation minutes used. The owner reads it over dinner in Bangkok.
Scenario 3: Large Commercial Operation, Tha Mai District
Owner: 180-rai commercial farm supplying direct to Chinese import buyers
Problem: Chinese buyers require GAP (Good Agricultural Practice) documentation including irrigation logs, pesticide records, and temperature data
FarmScript solution: Comprehensive logging to structured JSON, exportable as GAP audit trail
alerts {
log file: "/var/log/farmscript/orchard.log" format: json
log remote: "https://gap-audit.suanthongdee.th/api/logs" auth: bearer
}
Every sensor reading, actuator command, and alert is logged with timestamp, GPS zone, and sensor ID. The GAP documentation is generated automatically from the log file.
Why FarmScript Over a Mobile App or Config File?
A reasonable question: why build a programming language? Wouldn’t a mobile app with sliders and toggles be easier for farmers?
The answer depends on complexity. For a single pump on a timer — yes, a mobile app is simpler. But durian farming involves conditional logic that a UI can’t express cleanly:
- Irrigate Zone 1 in the morning, but only if moisture is below 55%, and only if we’re not in flowering stage, and only if there’s been no rain in the past 6 hours
- Send an alert if temperature exceeds 38°C, but escalate to SMS if it exceeds 40°C, but don’t alert at all between 11pm and 5am unless it’s a drought emergency
A mobile app with enough toggles and conditions to express this logic becomes its own programming language — just a bad one, with a confusing UI instead of a readable text file.
FarmScript gives you:
| Need | FarmScript Advantage |
|---|---|
| Complex conditional logic | Full when/and/or expression support |
| Multiple alert channels | Declarative alerts block, per-rule channel selection |
| Audit trail | Every action logged automatically |
| Version control | .farm files can be tracked in Git |
| Review by farm supervisor | Readable text anyone can verify |
| Deploy to multiple orchards | Parameterize and reuse rule blocks |
The Hardware Stack
FarmScript-generated code runs on commodity hardware available in Chanthaburi and Bangkok:
| Component | Specification | Cost (approx.) |
|---|---|---|
| Compute | Raspberry Pi 4 (4GB) | ฿2,200 |
| Soil moisture sensors (×4) | Capacitive, corrosion-resistant | ฿800 |
| Temperature/humidity | DHT22 × 2 | ฿300 |
| Relay board (8-channel) | 5V trigger | ฿450 |
| Flowering detector | Reed switch + float | ฿200 |
| Waterproof enclosure | IP65 junction box | ฿350 |
| 4G module + SIM | AIS/DTAC agricultural plan | ฿1,500 + ฿299/mo |
| Total hardware | ฿5,800 + ฿299/mo |
For a 45-rai orchard generating ฿3M+/season, this is a sub-0.2% investment in yield protection.
Code Walkthrough: Full Flowering Season Automation
Let’s trace a minimal but complete FarmScript program through the compiler.
Input: monthong_basic.farm
farm "Monthong Test Farm" {
timezone: "Asia/Bangkok"
}
sensors {
soil_moisture: analog pin A0 range 0..100 unit "%"
air_temperature: i2c device DHT22 unit "°C"
flowering_stage: visual pin D6 values [none, budding, full_bloom]
}
actuators {
pump: relay pin D7
mist_fan: relay pin D8
}
alerts {
line token: "YOUR_TOKEN"
}
rule irrigation {
when soil_moisture < 50 and flowering_stage == none {
turn on pump for 30 minutes
log "Irrigated. Moisture was {soil_moisture}%"
}
when flowering_stage == budding {
turn off pump
alert "Flowering detected — pump OFF" via line priority high
}
}
rule heat_protection {
when air_temperature > 38 {
turn on mist_fan
alert "Heat alert: {air_temperature}°C" via line
}
when air_temperature < 34 {
turn off mist_fan
}
}
Semantic Analysis Pass
✅ soil_moisture — declared, used in irrigation rule (numeric comparison ✓)
✅ air_temperature — declared, used in heat_protection rule (numeric comparison ✓)
✅ flowering_stage — declared, enum values [none, budding, full_bloom] ✓
✅ pump — declared, relay type, on/off commands valid ✓
✅ mist_fan — declared, relay type, on/off commands valid ✓
✅ line — configured in alerts block ✓
✅ No conflicting rules detected
✅ No undeclared references
Compilation successful.
Generated Python (abridged)
# generated/rules.py
from farmscript_rt import sensors, actuators, alerts, log, timer
def rule_irrigation():
soil_moisture = sensors.read("soil_moisture")
flowering_stage = sensors.read("flowering_stage")
if soil_moisture < 50 and flowering_stage == "none":
actuators.set("pump", state="on")
timer.set("pump", duration_minutes=30,
callback=lambda: actuators.set("pump", state="off"))
log(f"Irrigated. Moisture was {soil_moisture}%")
if flowering_stage == "budding":
actuators.set("pump", state="off")
timer.cancel("pump") # Cancel any active timer
alerts.line("Flowering detected — pump OFF", priority="high")
def rule_heat_protection():
air_temperature = sensors.read("air_temperature")
if air_temperature > 38:
actuators.set("mist_fan", state="on")
alerts.line(f"Heat alert: {air_temperature}°C")
elif air_temperature < 34:
actuators.set("mist_fan", state="off")
# generated/main.py
import time
from rules import rule_irrigation, rule_heat_protection
if __name__ == "__main__":
print("FarmScript runtime — Monthong Test Farm")
print("Polling interval: 30 seconds")
while True:
rule_irrigation()
rule_heat_protection()
time.sleep(30)
Clean. Readable. Deployable. The farmer writes 40 lines of FarmScript. The Raspberry Pi runs Python around the clock.
What’s Next for FarmScript
The Chanthaburi durian use case is our primary reference deployment. The roadmap ahead:
Near-term
- Thai-language keyword mode —
เมื่อ,เปิด,ปิด,แจ้งเตือนfor local farmer adoption - MicroPython target — compile to ESP32 for lower-cost single-zone deployments (฿350 vs ฿2,200)
- FarmScript Cloud — deploy and update
.farmprograms remotely without SSH
Medium-term
- Season mode — built-in awareness of durian phenological stages (vegetative, stress, flowering, fruit set, maturation, harvest, recovery)
- GAP report generator — automatic DoA-format agricultural records from log data
- Chanthaburi weather integration — pull TMD rainfall forecasts as a computed sensor input
Long-term
- Visual FarmScript editor — drag-and-drop rule builder that generates
.farmsource - Multi-farm dashboard — aggregate monitoring across orchards for commercial operators
- ML yield prediction — feed historical FarmScript logs into a yield forecasting model
Conclusion
Chanthaburi’s durian industry is one of Thailand’s most valuable agricultural exports — and one of the most technically demanding crops to grow. The difference between a premium export-grade harvest and significant yield loss often comes down to decisions made at exactly the right moment: when to stress the trees, when to cut irrigation, when to protect against heat.
FarmScript doesn’t replace the farmer’s expertise. It executes that expertise reliably, 24 hours a day, with full logging and instant alerts — so the farmer can focus on the decisions that actually require human judgment.
A programming language designed for durian farmers, compiled to Python, deployed on a ฿2,200 computer. That’s the entire stack.
Simplico Co., Ltd. is a Bangkok-based software engineering and product studio with 10+ years of enterprise delivery experience. We build custom IoT automation, AI systems, and agricultural technology for Thai and international markets.
Interested in deploying FarmScript for your Chanthaburi orchard or agricultural operation? Contact us at simplico.net
Tags: FarmScript DSL Chanthaburi Durian Smart Farming IoT Thailand Monthong AgriTech Raspberry Pi Python Compiler
Get in Touch with us
Related Posts
- FarmScript:我们如何从零设计一门农业IoT领域特定语言
- 智慧农业项目为何止步于试点阶段
- Why Smart Farming Projects Fail Before They Leave the Pilot Stage
- ERP项目为何总是超支、延期,最终令人失望
- ERP Projects: Why They Cost More, Take Longer, and Disappoint More Than Expected
- AI Security in Production: What Enterprise Teams Must Know in 2026
- 弹性无人机蜂群设计:具备安全通信的无领导者容错网状网络
- Designing Resilient Drone Swarms: Leaderless-Tolerant Mesh Networks with Secure Communications
- NumPy广播规则详解:为什么`(3,)`和`(3,1)`行为不同——以及它何时会悄悄给出错误答案
- NumPy Broadcasting Rules: Why `(3,)` and `(3,1)` Behave Differently — and When It Silently Gives Wrong Answers
- 关键基础设施遭受攻击:从乌克兰电网战争看工业IT/OT安全
- Critical Infrastructure Under Fire: What IT/OT Security Teams Can Learn from Ukraine’s Energy Grid
- LM Studio代码开发的系统提示词工程:`temperature`、`context_length`与`stop`词详解
- LM Studio System Prompt Engineering for Code: `temperature`, `context_length`, and `stop` Tokens Explained
- LlamaIndex + pgvector: Production RAG for Thai and Japanese Business Documents
- simpliShop:专为泰国市场打造的按需定制多语言电商平台
- simpliShop: The Thai E-Commerce Platform for Made-to-Order and Multi-Language Stores
- ERP项目为何失败(以及如何让你的项目成功)
- Why ERP Projects Fail (And How to Make Yours Succeed)
- Payment API幂等性设计:用Stripe、支付宝、微信支付和2C2P防止重复扣款













