# Dog tracking collar simulator: a HEART-embedded system you can run and watch

**A demonstration only.** It simulates a dog, its owner, a tracking collar and its HEART. There is no collar and no dog. Every position is in metres east and north of a made-up home origin: there is no latitude or longitude anywhere in it, and it never names a real place.

One Python file, `dogCollarSimulator.py`, with only the standard library. Run it, register it on [HEART Hub](https://heartcortex.ai/monitor) with the code it prints, and watch its health on your own private page while it lives a simulated dog's day: sleep at night, rest and play in the garden, walks with the owner, now and then a slip through the gate and a wander alone, and a collar whose battery, GNSS, uplink, temperature and accelerometer sometimes fail.

It teaches one lesson above all: **where the dog is, is the most private thing the collar knows.** The position reveals where the owner lives and walks, and where the dog is right now. So it is `private`, and the dog leaving its safe zone is a **private event for the owner**, never an alert and never sent to HEART Hub. Only the collar's health (status and fault codes) goes there.

It also shows **one change that runs at once** and **two that wait for a person**: lost mode (more frequent reporting, nothing the dog feels) runs at once with the owner key; the LED and the buzzer act on a living animal, so they wait for the owner's proof of presence, and the buzzer has a welfare guard.

The case study behind it, step by step for a real product, is [`guides/Dog-Tracking-Collar.md`](../guides/Dog-Tracking-Collar.md).

**On a real device:** [Dog Tracking Collar, for real](https://heartcortex.ai/docs/real-devices/Dog-Tracking-Collar.md) shows the hardware we assume, where the HEART runs, the code to change, how to build and install it, the factory steps and what the owner sees. For illustration only.


| File | What it is |
|---|---|
| `dogCollarSimulator.py` | The simulator: dog, collar, HEART, the owner's changes, local HEART API, HEART Hub reporting |
| `test_e2e.py` | End-to-end test against a monitor site on your own computer |
| `README.md` | This file |

---

## 1. Download and run

You need Python 3.8 or newer. Nothing to install.

```sh
python3 dogCollarSimulator.py --scenario all --clock 09:00
```

It starts at once and prints three things:

```
  HEART Hub: register this simulated dog collar
    1. Sign in at https://heartcortex.ai/login with your email (a 6-digit code is sent to you).
    2. Open https://heartcortex.ai/register?id=8e2b4f1c07a9d356
    3. Enter the claim code  R3K8M-7XQBA-2PTVW-HN6DZ
    4. Watch it at https://heartcortex.ai/device/8e2b4f1c07a9d356  (private to you until you share it)

11:07:44 HEART API on http://127.0.0.1:43281/heart/v1/pulse  (owner key for private views: ...)
11:07:44 09:00 dog rest · collar on · resting · in safe zone · GNSS off · uplink home base · lost mode off ·
         battery 15.5 % (~279 h) · 29.2 Cel · HEART healthy
```

Every 15 seconds it prints one line: the simulated time of day, what the dog is doing, what the collar measures, and the HEART's status. When something goes wrong, the HEART says so. When the dog leaves the safe zone, the **collar's own firmware** tells the owner's app, privately; the HEART has nothing to say about it:

```
11:08:06 PRIVATE  zone.left: The dog has left the safe zone alone, 93 m from home. Turn on lost mode to follow it
         every 30 s. (waiting for the uplink, then to the owner's app only; never to HEART Hub)
11:08:16 HEART error    performance.too-hot: The collar is too hot. The collar is 46.1 Cel, above 45 Cel: too hot
         for its battery. The collar has warned the owner privately about the dog.
11:08:20 HEART resolved   performance.too-hot: the collar is too hot
```

Stop it with Ctrl-C. The device ID, claim code and key are kept in `dog-collar-monitor.json` (readable only by you), so the same simulated collar comes back next time. Delete that file to make a new device.

## 2. Register it and watch it on HEART Hub

1. **Sign in.** Open `https://heartcortex.ai/login` and enter your email address. A 6-digit code arrives; type it in. There are no passwords.
2. **Register.** Open the `register?id=...` link the simulator printed, and enter the **claim code**. The claim code is the proof that you hold the device: it is shown only on the simulator's console, and the site holds only its hash.
3. **Watch.** Your device's page, `https://heartcortex.ai/device/<ID>`, shows its status (healthy, degraded or failing) and its open and recent alerts, each with the plain-language meaning of its fault code. All your devices are at `https://heartcortex.ai/devices`.
4. **Share, if you want.** The page is private until you make a share link or make it public. Sharing it shares the collar's health, never where the dog is.
5. **Silence.** Stop the simulator, or wait for the staged uplink loss: after three missed Pulse intervals (90 seconds) the page shows a **Flatline**, and you get one email. The site checks for silent devices every 5 minutes, so the email can take a few minutes more. When it reports again, you get one more.

Until you register it, the site stores nothing: the simulator's reports are answered "not registered yet" and dropped. After registering, it can take up to five minutes for the first report to arrive, or less as soon as something changes.

**What the site sees.** Only the public Pulse and public alerts, and it keeps only the status and timing and each alert's fault code, severity and times. Never a position, a distance, the track, a safe-zone event, the activity, lost mode or a reading. A "the dog has left the safe zone" message is not a fault code, so it cannot reach the site at all.

**Your own monitor site.** If you would rather keep even the collar's health on your own machine, run your own HEART Hub server ([`ownMonitorServer/README.md`](../ownMonitorServer/README.md)) and point the simulator at it:

```sh
node ownMonitorServer/server.mjs &
python3 dogCollarSimulator/dogCollarSimulator.py --monitor-url http://127.0.0.1:8787 --scenario all --speed 5
```

## 3. The staged faults

With `--scenario`, each event starts at a set time, lasts a while, and is then put right by the owner, so you can watch an alert open, escalate and resolve. Times are in simulated seconds: at `--speed 1` they are real seconds. While a staged scenario that moves the dog is chosen, the random walks and wanders are off, so the staged ones are easy to follow.

| Scenario | Starts | Lasts | What you see |
|---|---|---|---|
| `lowbattery` | at once | 10 min | An old cell, starting at 15.6 % and losing charge by itself: `power.battery-low` (warning) within about 2 minutes; then the owner charges it |
| `wander` | 30 s | 3 min | The gate is open: the dog slips out and wanders off alone, then the owner fetches it. **No alert at all**: a private `zone.left` event (alone, with the distance and position) and `zone.returned`, for the owner's app only |
| `link` | 1 min | 4 min | Neither LTE-M nor the home base answers: `connectivity.link-lost` (warning). No reports meanwhile; the wander's private events wait on the collar for the owner's app |
| `off` | 4 min 40 s | 4 min 10 s | The buckle opens and the collar lies on the grass by the hedge: completely still (no breathing) and cooler than on a dog for 3 minutes, so `system.main-function-degraded` (warning, "may have slipped off the dog"). Then the owner puts it back on |
| `hotcar` | 9 min | 4 min | The owner leaves the dog in a parked car in the sun. The collar warns the owner privately (`heat.risk`, a fact about the dog), and above 45 Cel raises its own fault `performance.too-hot` (error, a fact about the collar). Then the owner is back and drives home |
| `stuck` | 13 min 20 s | 3 min 20 s | On a walk, the accelerometer gives the very same reading again and again while GNSS shows the collar moving: `sensors.stuck` (warning). Then the owner restarts the collar |
| `nofix` | 16 min 40 s | 4 min | The same walk goes into thick woods: 3 failed fixes in a row and 150 s with no position, away from home: `sensors.no-reading` (warning). Out of the woods, and home |
| `all` | | | All of the above |

If the collar had **no fix and no uplink at once, away from home**, or a battery at 3 %, it would raise `system.main-function-lost` (critical): the owner could not be told where the dog is. The staged events do not overlap that way, but the rule is there.

Without `--scenario`, life goes on at its own pace: sleep from 22:00 to 06:30, walks every 3 to 7 hours by day (20 to 45 minutes), play in the garden, a poor sky under trees now and then on a walk, an uplink loss every 1 to 4 days, and a wander every 3 to 10 days.

The "may have slipped off" check uses 3 minutes so that the demo is short. A real collar would wait much longer (about 30 minutes), and on a hot day, when a collar on the grass is as warm as one on a dog, it cannot tell at all. That is why it is a warning that says "HEART cannot be sure", not a certainty.

## 4. Options

| Option | Default | Meaning |
|---|---|---|
| `--scenario` | none | `lowbattery`, `wander`, `link`, `off`, `hotcar`, `stuck`, `nofix` (comma-separated) or `all` |
| `--speed` | 1 | Simulated seconds per real second. `--speed 5` runs the staged events in about 4.5 minutes (too fast for Flatline emails, which need real minutes) |
| `--clock` | now | Start the simulated clock at this local time, `HH:MM`. The dog sleeps at night, so `--clock 09:00` gives a busy morning |
| `--monitor-url` | `https://heartcortex.ai` | Any HEART Hub site, including your own. `http` only for `127.0.0.1` |
| `--no-monitor` | | Report to no site: local only |
| `--state-file` | `dog-collar-monitor.json` | Where the device ID, claim code and key are kept |
| `--port` | 43281 | The local HEART API (falls back to 7685; `0` for none) |
| `--seed` | random | Repeat the same simulated life |
| `--duration` | until Ctrl-C | Stop after this many real seconds |

## 5. The local HEART API

The simulator also serves the HEART API on your own computer, as a real collar's HEART would (see the case study, Step 1, for where that runs):

```sh
curl http://127.0.0.1:43281/.well-known/heart-card.json         # the Card: what it is (no capabilities in public)
curl http://127.0.0.1:43281/heart/v1/pulse                       # public health
curl http://127.0.0.1:43281/heart/v1/arrhythmias                 # public alerts: codes only
curl http://127.0.0.1:43281/heart/v1/properties                  # public values: the battery only
KEY=...   # the owner key it printed
A="Authorization: Bearer $KEY"
curl -H "$A" http://127.0.0.1:43281/heart/v1/properties          # all values: position, activity, lost mode, readings
curl -H "$A" http://127.0.0.1:43281/heart/v1/arrhythmias         # alerts with explanations
curl -H "$A" http://127.0.0.1:43281/heart/v1/rhythm              # the private events: zone.left, zone.returned, heat.risk
curl -H "$A" http://127.0.0.1:43281/heart/v1/track               # the private track: every GNSS fix
curl -H "$A" http://127.0.0.1:43281/heart/v1/actions             # every change you made (heart.action)
curl http://127.0.0.1:43281/heart/v1/lifecycle                   # running (public); with the key, what it allows
```

**The privacy of every value**:

| Value | Privacy | Why |
|---|---|---|
| Status; fault codes of open and recent alerts | `public` | The only things HEART Hub receives |
| `battery_pct`, `battery_status` | `public` | Harmless, and useful to anyone you share the page with |
| `battery_hours_left`, `collar_temp`, `gnss_state`, `gnss_satellites`, `gnss_hdop`, `uplink`, `uplink_rssi`, `report_interval_s`, `reports_queued`, `accel_dynamic`, `led_mode` | `internal` | The collar's health, for the owner and a repairer. None of them says where the dog is |
| `position_east_m`, `position_north_m`, `position_source`, `position_age_s`, `distance_from_home_m`, `in_safe_zone`, `home_base_in_range` | `private` | Where the dog is, and so where the owner lives and walks. `home_base_in_range` alone says whether the dog is at home |
| `activity` (resting, walking, running) | `private` | The dog's day, and so the owner's: when they walk, when the house is empty |
| `lost_mode` | `private` | Says the dog is lost right now |
| Safe-zone and heat events (`/heart/v1/rhythm`), the track (`/heart/v1/track`) | `private` | The most private of all: a history of where the dog has been |
| Actions (`/heart/v1/actions`) | `internal` envelope; arguments and results `private` | What the owner changed, and when |
| The owner's name, address, phone; the dog's name | not on the collar's HEART | It does not need them, so it never has them |

**Your own changes.** Lost mode is not physical, so with the owner key it runs at once. The LED and the buzzer are physical, so they wait for you:

```sh
curl -H "$A" -X POST -d '{"arguments":{"on":true}}' http://127.0.0.1:43281/heart/v1/capabilities/collar.lost_mode/invoke
         # 200, succeeded: GNSS and reports every 30 s. The result shows the cost, for example
         # "batteryHoursLeft": 279.5 before, 3.3 after (at 15 % charge), and the new average current
curl -H "$A" -X POST -d '{"arguments":{"mode":"blink"}}' http://127.0.0.1:43281/heart/v1/capabilities/collar.led/invoke
         # 202, awaiting-approval; the console shows a 6-character code
curl -H "$A" -X POST -d '{"decision":"approve"}' http://127.0.0.1:43281/heart/v1/actions/<ID>/decision
         # 403 presence-required: the key alone cannot approve it
touch button.press        # in the simulator's working directory: the owner confirmed. It runs.
curl -H "$A" -X POST -d '{"arguments":{"volume":40,"durationSec":3}}' http://127.0.0.1:43281/heart/v1/capabilities/collar.buzzer/invoke
         # 202, waits for the button the same way
curl -H "$A" -X POST -d '{"arguments":{"volume":80,"durationSec":3}}' http://127.0.0.1:43281/heart/v1/capabilities/collar.buzzer/invoke
         # 409 guard-failed: above the safe 50 % for a dog's ears
```

**About `button.press`.** In a real collar the proof of presence is the owner confirming the change on their phone with a **passkey** (WebAuthn, over the exact Action digest), because nobody presses a button on a dog's neck. This simulator does not check a passkey: it checks for a file in its working directory, and records the decision honestly as `presenceMethod: device-button`. An approval sent over the network with only the key is refused with `403 presence-required`; one with any `presence` object is refused with `403 presence-invalid`, because the simulator cannot verify a passkey.

**The buzzer's guard** is in the collar, not in HEART: never louder than 50 %, never longer than 5 s, never sooner than 60 s after the last time (`409 guard-failed`, checked when you ask and again after you confirm). Outside the hardware's range (0 to 100 %, 1 to 30 s) it is `422 out-of-range`. A waiting change expires after 10 minutes. Any other change (the safe zone, GNSS, the uplink, a property) gets `403 capability-not-allowed`: those are set in the owner's app, not through HEART.

Every message it serves is a valid HEART message: `test_e2e.py` checks each one against the official schemas.

### Sleep until a stated time

The collar can go into deep sleep and wake at a time you choose, to save its battery (module `lifecycle`, API Standard 13.2 rule 3). It can also restart.

```sh
curl -H "$A" -X POST -d '{"durationSec":600}' http://127.0.0.1:43281/heart/v1/lifecycle/sleep
         # 202: the Action, running, with nextReachableAt: when the collar answers again
curl -H "$A" -X POST -d '{"wakeAt":"2026-11-13T06:30:00Z"}' http://127.0.0.1:43281/heart/v1/lifecycle/sleep
         # the same, until a stated time (UTC)
curl http://127.0.0.1:43281/heart/v1/pulse
         # asleep: "connection refused". The collar answers nothing at all
touch button.press        # wakes it early, as the collar's button would
curl -H "$A" http://127.0.0.1:43281/heart/v1/actions/<ID>
         # awake: succeeded, with wokeBy timer or button
curl -H "$A" -X POST http://127.0.0.1:43281/heart/v1/lifecycle/restart
         # a graceful restart: away for about 3 s, then back
```

**What sleeping means.** Half a second after the reply, the simulator closes its HTTP port, so every call is refused. The collar stops its work: no GNSS, no safe-zone check, no reports to the owner's app, no heat warning, no HEART noticing, nothing to HEART Hub. Only its clock runs, and the battery drains at the tiny sleep current. The dog's day goes on. It wakes by its **timer** or by its **button**: those are its `wakeSources`. `wake` is not offered (`501`): a collar in deep sleep cannot hear a request to wake.

**Time.** `durationSec` counts simulated seconds, like the staged events: at `--speed 5`, `durationSec: 600` is 2 real minutes. `wakeAt` and `nextReachableAt` are real UTC times, like every time in a HEART message. `nextReachableAt` is the wake time plus 3 seconds to start again.

**Safety: when it will not sleep.** The collar's safety job is to tell the owner where the dog is, and to warn of a hot dog. Asleep, it can do neither, and it will not notice the dog leaving. So it refuses to sleep with `409 conflict` while any of these is true:

| Situation | Why it stays awake |
|---|---|
| A critical alert is open (`system.main-function-lost`) | Nobody would hear about the fault, and the dog could not be found |
| Lost mode is on | The owner is looking for the dog |
| The dog is outside the safe zone | The collar would stop following it |
| The collar has warned of heat (`heat.risk`) | The dog may be shut in a hot car |

`GET /heart/v1/lifecycle` with the key lists `sleep` in `allowed` only when none of these holds. A real collar should also wake on movement (the accelerometer's wake-up line) so that a dog that leaves while it sleeps is noticed. The standard has no wake source for that yet, so this simulator does not claim one: sleep it only when the dog is safely at home. HEART Hub cannot tell sleep from a dead collar: a sleep longer than three Pulse intervals (90 s) shows a **Flatline** there, and you get its email.

**Refusals** (rule 3):

| Request | Answer |
|---|---|
| An operation that is not a standard name or `x-...` | `404 not-found` |
| `wake`, `shutdown` or another standard operation not offered | `501 not-implemented` |
| No owner key | `401 unauthenticated` |
| An unknown field; `delaySec` above 0 (no `schedules` module); both or neither of `durationSec` and `wakeAt`; `durationSec` outside 1 to 604800; `wakeAt` in the past or more than 7 simulated days ahead; `depth` other than `deep` | `400 invalid-request` |
| `restart` with `mode: force` (destructive: it needs proof of presence) | `403 presence-required` |
| One of the safety situations above, or a sleep or restart already under way | `409 conflict` |

Sleep and restart are risk `write`, not physical, and you are a human with the owner key, so they run at once (Section 6.3). The Action is not `cancellable`: the collar cannot be reached to cancel it. A change waiting for your button is cancelled when the collar goes to sleep. The LED goes dark while it sleeps and comes back as it was.

## 6. How the code is built

```
Dog  ──►  Collar  ──(reads)──►  Heart  ──►  local HEART API      (127.0.0.1)
day,       GNSS, uplink,          watches     └►  HEART Hub    (outbound only: status and fault codes)
walks,     accelerometer,         owner ──(key)──► collar.lost_mode           (not physical: at once)
wanders    temperature, battery,  owner ──(passkey; here button.press)──► collar.led, collar.buzzer  (physical)
           safe zone ──► private events for the owner's app (never HEART Hub)
```

| Part | In the file | In a real product |
|---|---|---|
| `Dog` | The simulated world: the hour, the weather, the dog's day (sleep, rest, play, walks with the owner, wanders alone, a parked car), its position as a random walk round a made-up home | The real dog |
| `Collar` | The simulated product: GNSS fixes with satellites and HDOP, the home base and LTE-M uplink, the accelerometer, the temperature, the battery and its current in each mode, the safe-zone check and its private events, the LED, the buzzer and its guard | The collar's own firmware and the maker's app. **The safe zone and the position are the product's main job, not HEART's** |
| `Heart.tend` | `RULES` turn the collar's values into alerts with fault codes; `escalate()` tells a person | **The code you add** |
| `Heart.owner_lost_mode`, `owner_physical`, `button_press`, `decide` | The gate for the owner's changes: at once for lost mode; wait for presence for the LED and buzzer, then run the collar's guard again | The gate of a HEART Full Core, with a passkey |
| `monitor()` | Hello, then reports to HEART Hub; follows the site's `nextSec`; stops for a wrong key; says hello again if forgotten; silent while the uplink is down | The same, unchanged |
| `Heart.lifecycle_post`, `lifecycle_tick`, `sleep_refusal` | Sleep and restart: check the request, refuse while sleeping is unsafe, go quiet, wake by timer or button | The product's own deep sleep; the HEART checks, gates and records it |
| `handler()` | The HEART API: Card, Pulse, lifecycle, alerts, properties, private events, track, Actions, invoke, decision | The same, behind TLS |

The HEART reaches the collar through three setters only, `set_lost_mode`, `set_led` and `buzz`, and the two physical ones sit behind presence and the collar's guard. HEART never uses the LED or the buzzer by itself, not even to escalate: they act on the dog.

## 7. What code to add to make a real product HEART-embedded

For a real collar, the **noticing** runs on the collar's own microcontroller, with no network, and the **HEART API and HEART Hub reporting** run where there is power to spare: the home base or the owner's phone app (see Step 1 of the case study). Pick a starting point, then add four things.

**Starting points**

| Where | Start from |
|---|---|
| The collar's low-power microcontroller (for example a cellular-and-GNSS module with an ARM Cortex-M core) | The bare-metal C template `heart-templates/templates/c-baremetal` (HEART Line: about 1 KB of code, no heap, no stdio). It says each fault and its end in short lines that ride inside the collar's existing uplink. Today it raises only `system.main-function-lost`: add one line per fault code in the table of Step 5 |
| A collar with Wi-Fi and a bigger battery (an ESP32 board) | The ESP32 Full starter `heart-templates/templates/esp32-full` (its signal table, main-job check, approvals and Monitor reporting). It keeps Wi-Fi and an HTTPS server always on and has no low-power mode, so it suits a home base far better than a collar |
| The home base, or a maker's server | The bridge `heart-templates/templates/bridge` (turns HEART Line into HEART Basic over HTTP), with this file's `monitor()`; or the Python template `heart-templates/templates/python` |
| The owner's phone app | The Python or JavaScript template (`heart-templates/templates/javascript-node`) inside the app |
| For the gated LED and buzzer with a passkey | The Rust Core `heartd` (`heart-core-rs`), which has the full gate, presence checks and Actions (HEART Full) |

**The four things you add**

1. **Read the collar.** Replace `Dog` and `Collar` with reads from your firmware: the fuel gauge, the GNSS receiver's fix and satellite count, the modem's link state, the accelerometer, the temperature sensor. The position stays where it is: the HEART needs only *whether* there is a fix, never *where*.

   ```python
   class Readings:
       """CHANGE: fill these from your firmware. Read only, except the three settings behind the gate."""
       def refresh(self):
           f = your_firmware.status()             # your code
           self.battery, self.temp = f.battery_pct, f.temp_c
           self.gnss_state, self.sats, self.fails = f.gnss.state, f.gnss.satellites, f.gnss.failed_in_a_row
           self.pos_age = f.seconds_since_last_position      # an age, not a position
           self.link_down, self.home_base = not f.uplink_up, f.home_base_in_range
           self.accel = f.accel_xyz
   ```

2. **Your rules and thresholds.** Edit `RULES` and `EXPLAIN` with your own limits (the battery level that is really low, the temperature your cell can stand). Keep the standard fault codes (Part 3). Keep the dog's facts out of them: leaving the safe zone, a hot dog, a dog that is lying still are not collar faults.

3. **Your escalation route.** Change `escalate()` to a notification in the owner's app. Do not use the LED or the buzzer for it: they act on the dog.

4. **Your privacy table.** Go through `properties()` and give every value its level (Step 3 of the case study). The position, the track, the safe zone and the activity are `private`; only status, fault codes and the battery are `public`. Keep the safe zone's centre in the app, or encrypted on the collar, never in a HEART message to anyone but the owner.

Then keep `monitor()` as it is, store the device ID, claim code and key in the collar's secure storage, and print the claim code and a QR code of `https://<your monitor site>/register?id=<ID>` on a card in the box. Serve the API over TLS (or keep it on loopback), run the HEART-Rating client (`heart-rating/client/heart_rating.py`) and Part 9's tests, and claim HEART Basic, Autonomy Level A1.

## 8. Test it

From the project's top folder, with Node 20+ and Python 3 with `pip install --user "jsonschema[format]>=4.18" referencing`:

```sh
python3 dogCollarSimulator/test_e2e.py
```

It starts a HEART Hub site on this computer (port 8808), runs the simulator with every staged event at 5x speed from a simulated 09:00 (local API on port 43308), signs in, registers the device through the site's own form, and checks for about four and a half minutes that: every message is valid (private events and Actions included); public views hold only public data and no location at all; lost mode runs at once with the owner key and its Action shows the battery cost; an LED change and a buzz wait, are refused over the network with `403 presence-required`, and run after `touch button.press`; the buzzer guard refuses loud, long or too-soon buzzing; other changes are refused; the wander is a private `zone.left` event and never an alert; the hot car is both a private warning and the `performance.too-hot` fault; every staged fault reaches the owner's page with its fault code; no position, coordinate, safe-zone event, activity or lost mode reaches the site; and the page stays private. Before that, on a second simulator (port 43309, no staged events), it checks the lifecycle: the Card, every refusal, a sleep that refuses connections until its stated time and then says `succeeded`, a sleep woken early by `button.press`, and a restart. On the main one it checks that the collar will not sleep while lost mode is on or the dog is outside the safe zone.

---

Licence: Apache-2.0. Part of HEART, the open standard at [heartcortex.ai](https://heartcortex.ai).
