Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions apps/api_server/src/twfarmbot_api_server/handlers/pin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@

from __future__ import annotations

from twfarmbot_core.config import load_yaml_config
from twfarmbot_core.domain import Action

from watering_service.backends import farmbot


def _pin_mode(pin: int, requested: str | None) -> str:
"""Return the requested pin mode, falling back to the configured mode."""
if requested:
return str(requested)
try:
pins = load_yaml_config().get("pins", []) or []
for p in pins:
if int(p.get("pin", -1)) == pin:
return str(p.get("mode", "digital"))
except Exception: # noqa: BLE001
pass
return "digital"


def handle_read_pin(action: Action) -> Action:
pin = int(action.params["pin"])
mode = str(action.params.get("mode", "digital"))
mode = _pin_mode(pin, action.params.get("mode"))
value = farmbot.backend.read_pin(pin, mode)
return Action(kind=action.kind, params={
"pin": pin, "mode": mode, "value": value
Expand All @@ -19,7 +34,7 @@ def handle_read_pin(action: Action) -> Action:
def handle_write_pin(action: Action) -> Action:
pin = int(action.params["pin"])
value = int(action.params["value"])
mode = str(action.params.get("mode", "digital"))
mode = _pin_mode(pin, action.params.get("mode"))
seconds = action.params.get("seconds")
duration = None
if value == 1 and seconds is not None:
Expand Down
18 changes: 15 additions & 3 deletions apps/ui/src/twfarmbot_ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1121,7 +1121,7 @@ def _render_io() -> None:
cols = st.columns(min(3, len(sensors)))
for i, s in enumerate(sensors):
with cols[i]:
with st.container(border=True, height=170):
with st.container(border=True):
mode = s.get("mode", "analog")
st.markdown(
f"**{s['label']}** <span class='pill'>{mode}</span>",
Expand Down Expand Up @@ -1151,7 +1151,7 @@ def _render_io() -> None:
st.markdown("### ⚡ Actuators")
a, b = st.columns(2)
with a:
with st.container(border=True, height=320):
with st.container(border=True):
st.markdown("**💧 Irrigation**")
secs = st.number_input("Seconds", 0.1, 300.0, 2.0, 0.5, key="water_secs")
if st.button("Water", use_container_width=True, type="primary"):
Expand All @@ -1167,7 +1167,7 @@ def _render_io() -> None:
st.caption("Runs the pump for the selected duration.")

with b:
with st.container(border=True, height=320):
with st.container(border=True):
st.markdown("**🔌 Peripheral control**")
outputs = [p for p in named if p.get("kind") != "sensor"]
if not outputs:
Expand All @@ -1187,6 +1187,18 @@ def _render_io() -> None:
)

if mode == "analog":
presets = sel.get("presets") or {}
if presets:
st.caption("Presets")
preset_cols = st.columns(len(presets))
for idx, (pval, plabel) in enumerate(sorted(presets.items(), key=lambda x: int(x[0]))):
if preset_cols[idx].button(
f"{plabel} ({pval})",
use_container_width=True,
key=f"preset_{sel['pin']}_{pval}",
):
_do_pin_write(client, sel["pin"], int(pval), mode)

val_col, btn_col = st.columns([4, 1])
with val_col:
analog_value = st.slider(
Expand Down
7 changes: 4 additions & 3 deletions configs/dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ log_level: INFO
# the environment, or reference a different env var via api_key_env.
planning:
base_url: https://openrouter.ai/api/v1 # OpenAI-compatible endpoint
model: openai/gpt-4o-mini # tool-calling model
model: ibm-granite/granite-4.1-8b # tool-calling model
api_key_env: PLANNING_LLM_API_KEY # name of the env var that holds the secret
timeout_s: 120 # raise this for slower models
temperature: 0.0
Expand Down Expand Up @@ -38,15 +38,16 @@ safety:
watering:
pump_pin: 7

# Named pins for the UI: label → {pin, mode, kind, group}.
# Named pins for the UI: label → {pin, mode, kind, group, presets}.
# kind: "valve" | "servo" | "sensor" | "io"
# group: shown as a column header in the pin grid
# presets (optional): value → label map for analog pins, e.g. {0: "Off", 120: "Cut"}
# Every pin number must be unique; a single GPIO can't drive a valve and
# read a sensor at the same time.
pins:
- {label: "Pump", pin: 7, mode: digital, kind: valve, group: "Water"}
- {label: "Water solenoid", pin: 8, mode: digital, kind: valve, group: "Water"}
- {label: "Cutting end effector", pin: 4, mode: analog, kind: io, group: "Tools"}
- {label: "Cutting end effector", pin: 4, mode: analog, kind: io, group: "Tools", presets: {0: "Off", 120: "Cut", 255: "Max"}}
- {label: "Tool servo", pin: 10, mode: digital, kind: servo, group: "Tools"}
- {label: "Soil sensor", pin: 14, mode: analog, kind: sensor, group: "Sensors"}
- {label: "Light sensor", pin: 15, mode: analog, kind: sensor, group: "Sensors"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage

from spatial_service import format_world_context
from twfarmbot_core.config import load_yaml_config

from .tool_policy import ToolCategory
from .tool_registry import ToolRegistry
Expand Down Expand Up @@ -112,6 +113,7 @@ def __init__(
def chat_system_prompt(self) -> str:
parts = [_CHAT_HEADER]
parts.append(self._render_tool_section())
parts.append(_format_pin_context())
parts.append(_CHAT_FOOTER)
if self._propose_only:
parts.append(_PROPOSE_ONLY_APPENDIX)
Expand All @@ -125,6 +127,7 @@ def chat_system_prompt(self) -> str:
def planner_system_prompt(self) -> str:
parts = [_PLANNER_HEADER]
parts.append(self._render_tool_section(for_planner=True))
parts.append(_format_pin_context())
parts.append(_PLANNER_FOOTER)
return "\n".join(parts)

Expand Down Expand Up @@ -225,3 +228,37 @@ def _render_tool_section(self, for_planner: bool = False) -> str:
lines.append(f"- `{d.name}` — {d.policy.description}{approval}")

return "\n".join(lines)


def _format_pin_context() -> str:
"""Load named pins from config and format them for the system prompt."""
try:
pins = load_yaml_config().get("pins", []) or []
except Exception: # noqa: BLE001
pins = []
if not pins:
return ""
lines = ["\nConfigured GPIO pins (single source of truth):"]
for p in pins:
label = p.get("label", "unknown")
pin = p.get("pin", "?")
mode = p.get("mode", "digital")
kind = p.get("kind", "io")
group = p.get("group", "")
group_text = f" · {group}" if group else ""
presets = p.get("presets") or {}
preset_text = ""
if presets:
preset_items = ", ".join(
f"{v}={lbl}" for v, lbl in sorted(presets.items(), key=lambda x: int(x[0]))
)
preset_text = f" · presets: {preset_items}"
lines.append(
f"- pin {pin} · {label} · mode={mode} · kind={kind}{group_text}{preset_text}"
)
lines.append(
"When calling read_pin or write_pin, use the configured mode for the pin "
"unless the user explicitly asks for a different mode. For analog pins, "
"use the named preset values when the user refers to them."
)
return "\n".join(lines)
24 changes: 21 additions & 3 deletions services/planning_service/planning_service/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,31 @@ class FindHomeArgs(BaseModel):

class ReadPinArgs(BaseModel):
pin: int = Field(..., description="GPIO pin number to read.")
mode: str = Field(default="digital", description="'digital' or 'analog'.")
mode: str = Field(
default="digital",
description=(
"'digital' or 'analog'. If omitted, default to the mode configured "
"for this pin in the system config."
),
)


class WritePinArgs(BaseModel):
pin: int = Field(..., description="GPIO pin number to write.")
value: int = Field(..., description="0 or 1.")
mode: str = Field(default="digital", description="'digital' or 'analog'.")
value: int = Field(
...,
description=(
"Value to write. For digital pins use 0 or 1. For analog pins use "
"0..255 (PWM)."
),
)
mode: str = Field(
default="digital",
description=(
"'digital' or 'analog'. If omitted, default to the mode configured "
"for this pin in the system config."
),
)


class MountToolArgs(BaseModel):
Expand Down
Loading