HEART Embedded Systems logoHEART

Greenhouse irrigation: on a real device#

An irrigation controller with a HEART on its own ESP32-S3: the parts, the wiring, the code, how to build and install it, the factory steps, and what the grower sees.

For illustration only. The hardware below is an assumption, chosen so the steps can be concrete. Your board, pins, probes, valves and pump will differ. This product has not been built. The ESP32 Full starter's core is tested on a computer, but its ESP32 port has not yet been compiled or run on a chip. Nothing here is advice on electrical safety in wet places or on water-supply rules. Check every step on your own bench, and have an electrician wire the pump.

PlacementA: inside the product. The HEART runs on the controller's ESP32-S3, in the same firmware as the schedule, in its own low-priority task.
Start fromThe ESP32 Full starter (heart/embeddedstandard/esp32-full/v1.2611.3)
ClaimHEART Basic, Autonomy Level A1. HEART watches and tells; it changes nothing. The brain's Trials need HEART Full (see Section 9).
Try it firstThe simulator and the design case study

1. The device we assume#

PartAssumed choiceNotes
BoardAn ESP32-S3 module with an external antenna socket, such as the ESP32-S3-WROOM-1U, on the controller's own boardGlass, metal frames and wet plants weaken Wi-Fi: an external antenna helps. The S3 has spare ADC1 pins.
Soil moistureThree capacitive soil-moisture probes (the common "v1.2" kind, analogue out), one per zone, each with a 1 MΩ pull-down at the inputCapacitive probes do not corrode like resistive ones. The pull-down makes an unplugged probe read 0 V
Tank levelA 0.5 to 4.5 V hydrostatic level sensor at the bottom of the tank, through a 2:1 dividerGives the tank in per cent
Tank cut-offA float switch with two contacts, near the bottom of the tankOne contact is in series with the pump contactor's coil, so the pump cannot run dry whatever any software does. The other goes to the ESP32
PumpA mains pump with its own thermal protector, switched by a contactorWired by an electrician, behind an RCD
Pump temperatureA waterproof DS18B20 probe on the pump bodyRead by the firmware on 1-Wire
ValvesThree 24 V AC normally-closed solenoid valves, one per zone, switched by relays (or triacs with opto-isolators)Normally closed: with no power, no water flows
FlowA hall-effect flow meter on the main line, pulses counted by the firmwareShows whether water really flows when a valve is open
AirAn SHT40 temperature and humidity sensor on I²C
DoorA reed switch on the greenhouse doorPrivate: it shows when people are there
ButtonA sealed push button on the controller's box, wired to groundHeld 3 seconds it is Stop. A short press approves a waiting action, such as restoring the brain
Status light and buzzerA green LED and a small buzzer on the boxTell a person about a fault with no internet
PowerA 24 V AC transformer for the valves, and a rectifier and 5 V buck converter for the controllerSee Section 8 for its size
NetworkThe greenhouse Wi-Fi, 2.4 GHzReports to HEART Hub, outbound only

Example pin map#

SignalPinIn signals.json
Soil, zone 1GPIO1, ADC1 channel 0"source": {"protocol": "on-chip", "channel": 0, ...}
Soil, zone 2GPIO2, ADC1 channel 1"source": {"protocol": "on-chip", "channel": 1, ...}
Soil, zone 3GPIO4, ADC1 channel 3"source": {"protocol": "on-chip", "channel": 3, ...}
Tank level (2:1 divider)GPIO5, ADC1 channel 4"source": {"protocol": "on-chip", "channel": 4, ...}
Tank cut-off float, second contactGPIO6, external 10 kΩ pull-up"source": {"protocol": "on-chip", "channel": 6}, a boolean with "safety": true
Door reed switchGPIO7, external 10 kΩ pull-up"source": {"protocol": "on-chip", "channel": 7}, a boolean, private
I²C SDA, SCL (SHT40)GPIO8, GPIO9Read by your firmware, not by HEART
1-Wire (pump DS18B20)GPIO10Read by your firmware
Valve relays, zones 1 to 3GPIO11, GPIO12, GPIO13Driven by your firmware only
Pump contactor relayGPIO14Driven by your firmware only
Flow meter pulsesGPIO15Counted by your firmware (the PCNT unit)
ButtonGPIO16CONFIG_HEART_BUTTON_GPIO=16
Status LEDGPIO17CONFIG_HEART_LED_GPIO=17
BuzzerGPIO18Driven by your escalation code

ADC channel 2 (GPIO3) is skipped because GPIO3 is a strapping pin on the S3. On the S3, ADC1 channel n is GPIO n+1. Check your own board's pin map.

One owner for ADC1. The starter's configure_io() claims ADC1 with adc_oneshot_new_unit(), and ESP-IDF lets only one handle own a unit. On this page HEART reads the probes and the tank, and the firmware does not need them: its schedule runs on minutes, and its interlock is the float switch. If your firmware also reads ADC1, have it read the pins and publish the values as sym: sources instead, or share one handle (a change to the port).


2. Where everything runs#

┌──────────────────────── ESP32-S3: one firmware image ────────────────────────┐
│ Your controller firmware (high priority)    HEART (from the ESP32 starter)   │
│  schedule: minutes per zone per day          heart_tend task (low priority):  │
│  valves, pump, tank interlock (float),       checks every 30 s, alerts,       │
│  60 min/day per zone, 80 °C pump cut-out     LED and buzzer                   │
│  flow count, pump and air temperature        HTTPS API on port 43278          │
│        ▲  shared variables (SYMBOLS[]): read only     HEART Hub reports ─────────► heartcortex.ai
│        └── HEART writes nothing here                                          │      (outbound only)
└───────────────────────────────────────────────────────────────────────────────┘
Float switch contact 1 ── in series with the pump contactor's coil (no software in the way)
NVS (encrypted): device ID, claim code, key · owner key · TLS certificate and key · last Stop
  • One image. HEART's files are compiled into the controller's ESP-IDF project. The starter runs HEART as the task heart_tend at tskIDLE_PRIORITY + 1, so the watering task, at a higher priority, always runs first.
  • HEART changes nothing. The case study allows one change, the watering minutes of a zone, and only through a Trial that a person approved. The ESP32 starter has no Trials, so on this controller every signal is read-only, and the grower sets the schedule on the panel as before.
  • The interlocks are out of reach. The float switch cuts the pump in hardware, and the firmware keeps its own limits: the tank interlock, 60 minutes a day per zone, and the pump's 80 °C cut-out. HEART reads the float switch, marked "safety": true, and never writes it.

3. The code#

Copy the starter, then merge your controller firmware into it, or copy the starter's main/ files into your existing project.

FileWhat you do
main/port_esp32.cChange the lines marked CHANGE (below)
signals.jsonWrite it with heart_signals.py, then export main/heart_signals.h
main/heart_core.c, heart_core.h, heart_ed25519.cCopy unchanged
main/Kconfig.projbuild, sdkconfig.defaultsCopy; set values in idf.py menuconfig
Your controller codeAdd as more source files or a component in main/, started from app_main()

Start the watering first. The starter's app_main() returns early if HEART refuses to start (for example, with a signal table nobody sealed). Start your watering task before heart_init(), so a HEART that does not start never stops the watering. If your firmware already starts Wi-Fi, keep one of the two Wi-Fi start-ups, not both. The starter's start_network() uses ESP_ERROR_CHECK, which restarts the chip on an error: in a product, handle those errors instead.

3.1 Identity#

#define SYSTEM_ID "com.example.irrigation.gh-controller"   /* CHANGE, with signals.json */
#define NAME      "Greenhouse Controller"                  /* CHANGE */
#define VERSION   "1.0.0"                                  /* CHANGE */

The system ID is the one in the case study, Step 2. It names the product, never the grower or the site.

3.2 The values your firmware shares with HEART#

/* CHANGE: the firmware variables your signal table names as sym:... sources. The controller task writes them. */
volatile float pump_temp = 20, air_temp = 20, air_rh = 50, flow_lpm = 0, water_today_l = 0,
               minutes_z1 = 30, minutes_z2 = 30, minutes_z3 = 30, zones_no_flow = 0, pump_running = 0, wifi_up = 0;
static const struct { const char *name; volatile float *value; } SYMBOLS[] = {
    {"pump_temp", &pump_temp}, {"air_temp", &air_temp}, {"air_rh", &air_rh}, {"flow_lpm", &flow_lpm},
    {"water_today_l", &water_today_l}, {"minutes_z1", &minutes_z1}, {"minutes_z2", &minutes_z2},
    {"minutes_z3", &minutes_z3}, {"zones_no_flow", &zones_no_flow}, {"pump_running", &pump_running},
    {"wifi_up", &wifi_up}, {0, 0}};

The controller task updates these every second. zones_no_flow is worked out for HEART's sake: the number of zones whose valve has been open for 30 seconds with no flow on the meter. With one zone open at a time, that points at a stuck valve or a blocked line. This is an assumption of this page: the case study spots a stuck valve from the soil not getting wetter after two waterings, which a finished core could also do. The flow meter is quicker and more direct.

HEART reads all of these and writes none.

3.3 The signal table (signals.json, the important lines)#

{"name": "com.example.irrigation.gh-controller.soil_z1", "title": "Soil moisture, zone 1", "type": "number", "unit": "%",
 "range": {"min": 25, "max": 45}, "source": {"protocol": "on-chip", "channel": 0, "scale": -0.05823, "offset": 192.3},
 "privacy": "internal",
 "alerts": [{"when": "outside-range", "code": "system.main-function-degraded", "severity": "warning"},
            {"when": "stuck", "afterSec": 90, "code": "sensors.stuck", "severity": "error"}]},
{"name": "com.example.irrigation.gh-controller.tank_pct", "title": "Water in the tank", "type": "number", "unit": "%",
 "range": {"min": 20, "max": 110}, "source": {"protocol": "on-chip", "channel": 4, "scale": 0.03785, "offset": -12.5},
 "privacy": "internal", "alerts": [{"when": "outside-range", "code": "system.main-function-degraded", "severity": "warning"}]},
{"name": "com.example.irrigation.gh-controller.tank_cutoff", "title": "Tank below the pump cut-off",
 "description": "1 = below the float, or its wire is broken. The float also cuts the pump in hardware. HEART only reads it.",
 "type": "boolean", "source": {"protocol": "on-chip", "channel": 6}, "privacy": "public", "safety": true},
{"name": "com.example.irrigation.gh-controller.pump_temp", "title": "Pump temperature", "type": "number", "unit": "Cel",
 "range": {"min": -10, "max": 70}, "source": {"protocol": "on-chip", "symbol": "pump_temp"}, "privacy": "internal",
 "alerts": [{"when": "outside-range", "code": "performance.too-hot", "severity": "warning"}]},
{"name": "com.example.irrigation.gh-controller.zones_no_flow", "title": "Open zones with no water flowing", "type": "integer",
 "range": {"min": 0, "max": 0}, "source": {"protocol": "on-chip", "symbol": "zones_no_flow"}, "privacy": "internal",
 "alerts": [{"when": "outside-range", "code": "actuators.not-responding", "severity": "error"}]},
{"name": "com.example.irrigation.gh-controller.minutes_z1", "title": "Watering, zone 1", "type": "integer", "unit": "min",
 "range": {"min": 10, "max": 60}, "source": {"protocol": "on-chip", "symbol": "minutes_z1"}, "privacy": "internal"},
{"name": "com.example.irrigation.gh-controller.door_open", "title": "Door open", "type": "boolean",
 "source": {"protocol": "on-chip", "channel": 7}, "privacy": "private"}

Zones 2 and 3 repeat the soil and minutes lines, on channels 1 and 3. The table also lists air_temp, air_rh, flow_lpm, water_today_l, pump_running and wifi_up, all internal, as in the case study, Step 3. No signal is writable. The grower's name, address and crop contracts are not on the controller at all.

The float contact closes to ground while there is water above it, so the pin reads 0. A low tank or a broken wire reads 1, which is the safe way round. The door reed switch is wired the same way: 1 is open.

The scale and offset turn the raw ADC reading into per cent. The starter reads 12 bits at 12 dB attenuation: 0 to 4095 for about 0 to 3.1 V, so one step is about 3.1 / 4095 = 0.757 mV.

  • Soil: say a probe gives 2.5 V in dry air (raw 2.5 / 0.000757 = 3302) and 1.2 V in water (raw 1585). Then per cent = (3302 − raw) / (3302 − 1585) × 100 = raw × −0.05823 + 192.3. Check: raw 3302 gives 0 %, raw 1585 gives 100 %. These end points are examples: measure your own probes in dry air and in water. The result is a per cent of the probe's span, not the water content of the soil, so set the band of 35 to 45 % (and the 25 % floor) from your own crops, in the same units. An unplugged probe reads 0 V, which is 192 %, so it shows as out of range.
  • Tank: the sensor gives 0.5 V empty and 4.5 V full, which the 2:1 divider makes 0.25 to 2.25 V, raw 330 to 2972. So per cent = (raw − 330) / (2972 − 330) × 100 = raw × 0.03785 − 12.5. Check: raw 2972 gives 100.0 %. The range goes up to 110 % so a small calibration error on a full tank is not a fault.

Calibrate both on your bench, with a multimeter, a dry probe, a glass of water and a measured tank, because every chip's ADC differs a little.

What the starter raises. The starter's core (heart_core.c) raises each range alert with the code and severity that the signal's line in the table names, and the summary names the signal's title ("Water in the tank reads outside its normal range..."). It does not check for stuck readings (the starter's Known gaps), and it checks each reading every 30 seconds, not the 5-minute mean the case study uses: a reading just after watering may briefly go over 45 %. system.main-function-lost from Section 3.4 comes from the product code. Sections 6 and 7 show what the starter raises, and the code a finished product raises where it differs.

A table can also name only one code for each kind of alert. The case study has two for the soil: too dry (below 25 %) is system.main-function-degraded, too wet (a 5-minute mean above 45 %) is performance.overshoot. The table above names the first; a finished core needs its own rule, or a second signal, for the second.

Then seal it. The HEART refuses to start with a table nobody reviewed, or one that makes a safety signal writable:

python3 heart_signals.py check signals.json
python3 heart_signals.py seal signals.json --by "Your Name"
python3 heart_signals.py export signals.json --out main/heart_signals.h

3.4 The main job, escalation and lifecycle#

/* CHANGE: return 1 while the product does its main job: every zone can get the water its schedule says. */
int heart_port_main_job_ok(const char **why) {
    if (pump_locked_out) { *why = "The tank is empty: the pump is locked out, nothing can be watered."; return 0; }
    return 1;
}

/* CHANGE: tell a person, with no internet: blink the LED and sound the buzzer on the controller's box. */
void heart_port_escalate(const char *summary) {
    ESP_LOGE(TAG, "escalate: %s", summary);
    fault_blink = 1; buzz_count = 3;
    grower_queue_message(summary);         /* your code: app or SMS, sent by another task; never wait here */
}

/* CHANGE: the controller runs the schedule, so it never sleeps; a restart waits until no valve is open. */
int heart_port_lifecycle_ready(const char *operation) {
    if (!strcmp(operation, "sleep")) return 0;
    return !any_valve_open && !pump_running;
}
  • The main job is the case study's check. pump_locked_out is your firmware's own flag, set from the float switch. Less than that, a low tank or dry soil, is degraded, and comes from the signal table.
  • Escalation is called while the core holds its lock, so it only sets flags and queues the message.
  • A restart waits until watering has finished. If that does not happen within timeoutSec (default 60), the restart fails and is never forced. A sleep request always fails, by design: a sleeping controller would miss its watering.

4. Build and install#

You need ESP-IDF 5.2 or later, and a USB-C cable.

idf.py set-target esp32s3
idf.py menuconfig

In menuconfig, under HEART:

SettingValue for development
Wi-Fi name and password (HEART_WIFI_SSID, HEART_WIFI_PASSWORD)Your test network. Products get these from the setup app, never from sdkconfig.
Report health to a HEART Hub site (HEART_MONITOR)On
HEART Hub site (HEART_MONITOR_URL)https://heartcortex.ai, or your own monitor site
Approvals on the monitor site (HEART_MONITOR_APPROVALS)Off. Nothing on this controller is writable
AI brain (HEART_BRAIN)Off, unless you want alerts explained by a model (Section 9)
Button GPIO, LED GPIO (HEART_BUTTON_GPIO, HEART_LED_GPIO)16 and 17
Reasoning Gateway URL (HEART_GATEWAY_URL)Empty

Make a development certificate, then build and flash:

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 825 \
  -keyout main/certs/prvtkey.pem -out main/certs/servercert.pem -subj "/CN=greenhouse.local"
idf.py build flash monitor

At first start the console shows the owner key once, and the HEART Hub lines:

==== HEART Hub ====
Register this device at https://heartcortex.ai/register?id=a41f6e2c9b07d358
Claim code: GH7PK-2MXQR-9TCWB-4NVZD
=======================

Check it from a computer on the same network (heart- and the last three bytes of the controller's MAC address):

curl -k https://heart-5e9a03.local:43278/heart/v1/pulse

Installing it. Mount the box out of direct sun and spray, with its cable glands at the bottom. An electrician connects the pump and its contactor, behind an RCD. Fit the float switch so its cut-off is above the pump's intake.


5. On the production line#

For each controller:

  1. Flash the release firmware, with flash encryption and secure boot turned on, so the keys in NVS cannot be read off the chip.
  2. Write its own TLS certificate and key to NVS (namespace heart, blobs cert and key). Never ship the development certificate.
  3. Start it once on the test jig. It makes its device ID, claim code and key, and stores them in NVS.
  4. Read the device ID and claim code from the jig's console, and print the label for inside the box's lid: the claim code, and a QR code of https://heartcortex.ai/register?id=<ID>. In port_esp32.c, replace the console line with your jig's label printer (the CHANGE at heart_port_monitor_load).
  5. Check the outputs fail safe on the jig: with the ESP32 held in reset, every valve relay and the pump relay must be off.
  6. Run the bench tests of Section 7 on a sample from each batch.

6. What the grower sees#

  1. Setup. The maker's app joins the controller to the greenhouse Wi-Fi, as before. The grower sets the schedule on the panel, as before.
  2. Registration. The grower scans the QR code inside the lid, signs in at heartcortex.ai with an emailed code, and types the claim code. The page shows Healthy. It is private until the grower shares it. See the owner's manual.
  3. Every day. Nothing. The controller checks itself every 30 seconds.
  4. When something goes wrong:
Real eventThe HEART notices (with the starter)Code a finished product raisesThe grower sees
The refill fails and the tank runs dryBelow the float: the pump is locked out; system.main-function-lost: "The tank is empty..."The sameThe LED blinks and the buzzer sounds; the page shows Failing
The tank is getting lowBelow 20 %: system.main-function-degraded on "Water in the tank"system.main-function-degradedA warning, before watering stops
A hot spell dries a zoneBelow 25 %: system.main-function-degraded on "Soil moisture, zone 2"system.main-function-degradedA warning: plants will wilt
A zone stays too wetAbove 45 %: system.main-function-degraded, the table's one code for that signalperformance.overshootA warning: roots can rot. The grower changes the minutes on the panel
A valve sticks, or a line is blockedOpen with no flow: actuators.not-responding (an error) on "Open zones with no water flowing"actuators.not-respondingA warning naming the check
A probe is knocked out of its cableReads about 192 %: system.main-function-degraded, the soil signal's codesensors.out-of-rangeA warning on that zone's probe
A probe's reading freezesNothing yet: the starter does not check for stuck readingssensors.stuckNothing, until the core can do this
The pump's bearing fails and it runs hotAbove 70 °C: performance.too-hot on "Pump temperature"The sameA warning; at 80 °C the firmware's own cut-out stops the pump
The greenhouse Wi-Fi goes downNothing can be sent. Watering goes on as scheduledconnectivity.link-lostAfter 90 seconds the page shows Not heard from, and one email
  1. Changing the watering. The grower changes the minutes on the controller's panel, as before. A request to change a signal through the HEART API is refused with 409 conflict (the signal is read-only), as the case study asks. Only the public Pulse and public alerts (health and fault codes) leave the greenhouse for HEART Hub: never a soil reading, the tank level, the schedule or a door visit.

7. Test it on the bench#

Build a small test rig: a tank, the pump, one or two valves over a tray, and pots of soil for the probes. Cause every fault for real, and check the right code appears within a minute, on the controller's API and on its HEART Hub page.

FaultHow to cause it on the benchExpect with the starterExpect from a finished product
Tank emptyDrain the test tank below the floatThe pump does not start, even when the firmware asks; system.main-function-lostThe same
Tank lowDrain to 15 %system.main-function-degradedThe same
Soil dryLift a probe out of its pot into dry airsystem.main-function-degradedThe same
Soil wetStand a probe in a glass of watersystem.main-function-degradedperformance.overshoot
Probe unpluggedPull the probe's plug while it runssystem.main-function-degradedsensors.out-of-range
Probe stuckCover a probe's pot with film, and water another pot on its zoneNothingsensors.stuck
Valve stuckClose the hand valve before zone 1's solenoid, or unplug the solenoid, then run zone 1actuators.not-responding on "Open zones with no water flowing"The same
Pump hotPut the pump's DS18B20 in water at 75 °Cperformance.too-hotThe same
Wi-Fi lostSwitch off the access point for 2 minutesNot heard from on the page, then Healthy again. Watering goes onThe same
Power cutSwitch off the supply while a zone is wateringEvery valve closes and the pump stops; after power returns the schedule carries onThe same
HEART cannot startFlash a build whose signal table is not sealedHEART refuses to start. Watering must work exactly as beforeThe same
No direct changesPUT a new value for minutes_z1 with the owner key409 conflict (the signal is read-only); nothing changesThe same
StopHold the button for 3 secondsGET /heart/v1/tending/autonomy shows "max": 2 and "mode": "observe". Watering goes on: Stop halts HEART's own actions, not the controllerThe same

Then run the HEART-Rating client against the controller, and claim only what passed.


8. Water, power and safety: what to expect#

  • HEART never sits between the pump and its protection. The float switch cuts the pump's contactor in hardware, the firmware's interlock does it again, and the pump has its own thermal protector. HEART reads the float switch, marked "safety": true; the starter refuses a table that makes such a signal writable. The case study puts the interlock in the firmware; the hardware contact on this page is an addition, so a firmware fault cannot run the pump dry either.
  • Everything fails closed. The valves are normally closed and the relays are off at reset. A power cut, a crash or a restart stops the water; it never leaves a zone running.
  • A task is not a process. A HEART that is slow or stuck cannot delay the watering, because its task has the lower priority. But a HEART that crashes the chip restarts the whole controller. The valves close, and the schedule carries on after the restart. For a guarantee that HEART can never interrupt watering, use two chips, or a Linux controller where heartd is a separate process.
  • Power, roughly. Typical 24 V AC irrigation valves draw about 0.4 A when they open and 0.25 A to stay open (check your valve's datasheet). One valve opening is about 24 × 0.4 ≈ 10 VA. The ESP32-S3 with Wi-Fi on draws roughly 0.4 to 0.8 W, and a few relay coils about 0.4 W each, so the controller needs about 3 W at most. One zone at a time: about 13 VA, so a 20 VA transformer leaves margin; allow 40 VA if two zones may open together (2 × 10 + 3 = 23 VA).
  • Water rules. A tank fed from the mains needs backflow prevention, and pumps in wet places need the local electrical rules. HEART conformance says nothing about either.

9. Going further#

  • Explanations from an AI model. Turn on AI brain in menuconfig. The monitor site then acts as the controller's Command Center and holds the model keys, under a brain lease that the owner can cut at any time. The controller asks it to explain an alert, shows the answer as a suggestion only, and never acts on it. Only public and internal values are sent, so the door never reaches a model. See the starter's Section 5b.
  • The brain proposes, the HEART decides. The simulator's Trials ("water 20 % less, 30 → 27 → 24 min/day, in two steps") need HEART Full: Consultations, Proposals, Trials with their own undo, the owner's envelope, and learned Playbooks that stay proposed until a person adopts them. The ESP32 starter has none of these yet. A Linux controller, or a small Linux computer beside this one, can run heartd: its consult.rs and trial.rs implement them. Watering is physical, so every Trial still waits for a press at the greenhouse, whatever the envelope says.
  • Approving from away. Once a Linux HEART offers Trials, remoteSitePhysical lets the grower approve on the monitor site with an emailed code. It is off by default, needs a second opt-in at the controller, and proves only that the grower could read their inbox, not that anyone looked at the plants. See the owner's manual, Section 9.
  • The guide's own fault codes. Until the core raises the codes named in the table, a maker can add checks for a low tank, dry or wet soil, stuck probes and the valve check to the core, keeping the standard codes of Part 3. Report what you change.
  • A commercial controller you cannot change. Put a small computer beside it, read it over Modbus or RS-485, and run the Python template or heartd there, configured as in its configuration guide.