Isaac Tools
Account

Protocol reference

Generated from packages/protocol/SPEC.md, the normative text. The TypeScript codec, the C header, and the agent's Rust codec implement it.

This is the normative specification of the framed packet protocol every Isaac Tools board speaks to the host over USB serial, UART board-to-board, MQTT, and BLE. It is a seam artifact: the TypeScript codec (packages/protocol/src), the C header (packages/protocol/c/isaac_proto.h), and the Rust codec (apps/agent/src/protocol) implement it, and packages/protocol/test/fixtures/frames.golden.json holds frames that all three must encode and decode byte for byte. Changes to this file go through a seam/ branch on main.

Conventions: all multi-byte integers are little-endian. u8, u16, u32 are unsigned; i16, i32 are two's complement; f32 is IEEE 754 binary32. char[n] is an ASCII field of exactly n bytes, NUL-padded on the right, not required to be NUL-terminated. bytes is a byte string that runs to the end of the payload unless a length precedes it. pcm16 is signed 16-bit mono audio at 16 kHz.

1. Frame

offset  size  field
0       2     magic 0xA5 0x5A
2       2     length of the payload only (u16), 0..1024
4       1     type (section 2)
5       1     seq
6       n     payload
6+n     2     crc (u16): CRC-16/CCITT-FALSE over bytes 2 .. 6+n-1 (length, type, seq, payload)

A frame is 8 + n bytes; the largest frame is 1032 bytes. seq is a per-sender counter that starts at 0, increments by one per frame sent, and wraps from 255 to 0. A receiver does not require seq to be consecutive; seq exists so a NACK can name a frame.

1.1 CRC-16/CCITT-FALSE

Polynomial 0x1021, initial value 0xFFFF, no input or output reflection, no final XOR. Reference check: the ASCII string 123456789 has CRC 0x29B1. The CRC is stored little-endian in the frame, so a frame whose CRC is 0x29B1 ends with the bytes B1 29.

Bitwise reference:

crc = 0xFFFF
for each byte b:
    crc ^= b << 8
    repeat 8 times:
        crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1)
        crc &= 0xFFFF

1.2 Receiving: resync and bad frames

A receiver treats its input as a byte stream and runs this state machine:

  1. Scan for the magic bytes 0xA5 then 0x5A. Bytes before them are discarded silently.
  2. Read the length. If it is greater than 1024, this is not a frame: discard the two magic bytes, and resume scanning from the first length byte (the bytes already read are not lost; they may begin a real frame). No NACK is sent, because the seq byte has not been read and could not be trusted.
  3. Read type, seq, payload, and crc.
  4. Verify the CRC. On a mismatch, discard the whole frame as delimited by its length field, send NACK with the seq byte as received, and resume scanning after the frame. A corrupted length field can make this step swallow part of the next frame; the sender's next frames resynchronize the stream.
  5. On a match, deliver the frame.

A partial frame that receives no further bytes for 200 ms is discarded by the transport (the decoder is reset) without a NACK. A receiver sends at most one NACK per bad frame and never sends NACK in response to a NACK.

1.3 Unknown and malformed messages

A frame with an unknown type is answered with ERR code 1 (BAD_TYPE) and dropped. A frame whose payload does not match the layout of its type (too short, a count that overruns the payload) is answered with ERR code 2 (BAD_LENGTH) and dropped. Trailing bytes beyond the declared layout are ignored, so a later protocol version can append fields.

2. Message types

Indexes actuator_idx and sensor_idx are the zero-based positions in the body file's actuators and sensors arrays, over all boards (section 3.4). packages/protocol exports indexMaps(body) and the firmware generator emits named constants for them.

type name direction payload
0x01 HELLO board to host u8 proto_ver, u8[32] body_hash, char[16] board_id, u32 fw_crc [, u8 module_variant, u8 hw_revision]
0x02 HELLO_ACK host to board u8 proto_ver, u32 host_time_ms
0x03 PING both u32 t_ms
0x04 PONG both u32 t_ms_echo
0x10 CMD_ACTUATOR host to board u8 count, { u8 actuator_idx, i16 value }[count]
0x11 CMD_STOP host to board empty
0x12 CMD_DISPLAY host to board u8 op, bytes args (section 2.3)
0x13 CMD_AUDIO_PLAY host to board u16 chunk_idx, u16 total, pcm16 samples
0x14 CMD_MODE host to board u8 mode
0x20 SENSE board to host u32 t_ms, u8 count, { u8 sensor_idx, u8 kind, value }[count]
0x21 ENC board to host u32 t_ms, u8 count, { u8 actuator_idx, i32 ticks }[count]
0x22 AUDIO_IN board to host u16 chunk_idx, pcm16 samples
0x23 EVENT board to host u8 event_id, bytes args (section 2.5)
0x30 CAL_SET host to board bytes cbor calibration subset (section 2.6)
0x31 CAL_GET host to board empty
0x32 CAL_DATA board to host bytes cbor full calibration
0x40 SKILL_LOAD host to board u16 chunk_idx, u16 total, bytes chunk
0x41 SKILL_STATUS board to host u8 state, u16 skill_crc
0x50 LOG board to host u8 level, char[] text
0x60 PAIR_CHALLENGE host to board u8[32] robot_id, u8[20] owner, u8[32] nonce, u64 chain_id, u8[20] contract, u16 timeout_s (section 2.9)
0x61 PAIR_RESULT board to host u8[20] device_key, u8[65] signature, u8[32] digest, u8 status (section 2.9)
0x62 PROVISION host to board bytes cbor network configuration (section 2.9)
0x63 PROVISION_ACK board to host u8 status, char[] text (section 2.9)
0x7E NACK both u8 bad_seq
0x7F ERR both u16 code, char[] text

char[] without a length runs to the end of the payload, is UTF-8, and is not NUL-terminated.

2.1 Session

proto_ver is 1. A board sends HELLO when its link comes up and every 2 s until it receives HELLO_ACK. The handshake belongs to a link, not to the board: a board with more than one transport (USB serial plus MQTT, say) sends HELLO at once on every new MQTT session, because the host on the broker has not seen it even when a USB host already answered, and keeps repeating it until a host answers. A board that can tell its links apart tracks HELLO_ACK per link and repeats HELLO only on the links still unanswered (the generated ESP32 scaffolds do); one that cannot clears its acknowledgement on each MQTT link-up and repeats on all links until any host answers (the Isaac firmware does). body_hash is the 32-byte body hash the board was built for (section 4.13 of the build document); the host compares it with the robot's body file and refuses teleop on a mismatch. board_id is the board's id from the body file. fw_crc is the CRC-32 (IEEE, as zlib computes it) of the firmware image, for the agent's flash status. host_time_ms lets the board offset its t_ms clock; t_ms in SENSE, ENC, PING is the board's monotonic millisecond counter, wrapping at 2^32.

Revision 1.2 appends two optional bytes to HELLO (55 bytes instead of 53), for hardware that can be built in more than one way:

field meaning
module_variant the module the firmware found at boot: 0 unknown, 1 ESP32-S3-MINI-1-N4R2 (PSRAM present), 2 ESP32-S3-MINI-1-N8 (no PSRAM). Detected by the firmware (PSRAM probe), not configured
hw_revision the board revision as an ASCII letter ('A' = 0x41), 0 unknown. The board has no revision ID: the firmware takes it from the body file it was built from (boards[].pcb.revision); one body file per revision, so the body hash identifies the revision too

A host reads them when the payload is 55 bytes or longer and treats them as unknown on a 53-byte HELLO (a revision 1.0 or 1.1 board); a 1.1 host ignores them (section 1.3).

2.2 Actuators and modes

CMD_ACTUATOR carries up to 255 entries. value is -1000..1000 for DC motors (signed fraction of full power in thousandths), the pulse width in microseconds for servos, and steps per second for steppers. A board clamps out-of-range values. Actuators not named in a CMD_ACTUATOR keep their last value. CMD_STOP sets every actuator to zero; a board also executes it on its own after 500 ms without a CMD_ACTUATOR or PING in teleop mode (section 4).

CMD_MODE modes: 0 idle (actuators off, sensors streaming), 1 teleop (CMD_ACTUATOR accepted, safety stop armed), 2 skill (an on-board rule skill drives the actuators; CMD_ACTUATOR is rejected with ERR 4), 3 calibrate (calibration steps may drive actuators; no safety stop). A board starts in mode 0.

2.3 CMD_DISPLAY ops

op name args
0x00 FACE u8 expression (section 2.4)
0x01 TEXT u8 len, char[len] utf8 short caption under the eyes
0x02 PAIR_PROMPT empty; the device shows the pairing prompt and waits for the talk button
0x03 NOTIFY u8 event_id from section 2.5 (paired, reward, sale, verified), pushed by the host
0x04 BRIGHTNESS u8 level 0..255 backlight
0x05 MOOD u8 valence, u8 arousal each 0..255 for 0..1
0x06 CLEAR empty

Boards without a display role answer CMD_DISPLAY with ERR 8 (UNSUPPORTED).

2.4 Face expressions

The expression byte is the index into the face output's values list of companion/1 (packages/body-spec/interfaces/companion-1.json): 0 neutral, 1 blink, 2 happy, 3 sleepy, 4 surprised, 5 thinking, 6 listening, 7 speaking, 8 sad, 9 dizzy, 10 low_battery, 11 charging, 12 paired, 13 reward.

2.5 Events

EVENT.event_id and NOTIFY share one table, which is the event enum of the rule-skill schema (packages/body-spec/schema/rules.schema.json) in order:

id event args origin
1 button_talk u8 pressed 1 down, 0 up board
2 button_a u8 pressed board
3 button_b u8 pressed board
4 tap empty board
5 double_tap empty board
6 shake empty board
7 pickup empty board
8 setdown empty board
9 low_battery u8 percent board
10 charging empty board
11 charged empty board
12 paired empty (see 2.9) board
13 reward empty host
14 sale empty host
15 verified empty host

Host-origin events reach the board as CMD_DISPLAY NOTIFY and are fed to the on-board rule runtime as events of the same id.

2.6 SENSE value kinds

kind encoding size used for
0 i16 2 raw ADC and line sensor levels
1 i32 4 counters
2 f32 4 distances in meters, battery 0..1, temperature in C, cell voltage in V, charge rate in %/h
3 f32[3] 12 motion: acceleration x, y, z in m/s^2
4 u8 1 buttons and touch as bit masks, booleans, the charger state bits

A SENSE frame carries one reading per listed sensor; a board may split its sensors over several frames per tick. Roles map to kinds as follows: motion is kind 3; distance_* and battery and temperature are kind 2; line_* and light are kind 0 (raw level, the host normalizes with calibration); button and touch are kind 4; encoder as a sensor is kind 1 (encoders on actuators use ENC).

Revision 1.2 adds the power roles of a head with a fuel gauge and a charger (the Isaac head, revision A). They are ordinary sensors of the body file, so their sensor_idx is their position in sensors[]:

role kind value
battery 2 state of charge 0..1 (as before)
battery_voltage 2 cell voltage (VCELL) in volts; NaN while the gauge does not answer
charge_rate 2 charge rate (CRATE) in %/h, negative while discharging; NaN while the gauge does not answer
temperature 2 pack temperature in C; the entry is left out of SENSE while the board has no valid reading
charger 4 the charger state bits below

A board leaves a reading out of SENSE (rather than sending NaN) when the hardware cannot measure it in its current state; the pack NTC of the Isaac head, for example, is only biased while charging, during a charge-enable pulse, or on battery. Charger state bits:

bit mask name set when
0 0x01 usb USB power is present (the charger's PGOOD)
1 0x02 charging the charger is charging the cell
2 0x04 charged USB present, the charge terminated on its own and the cell is full
3 0x08 paused_thermal the firmware paused charging because the pack is outside its temperature window
4 0x10 paused_firmware the firmware paused charging for another reason
5 0x20 restricted restricted mode: USB present without a confirmed cell, the board limits radio power, CPU, brightness and volume
6 0x40 fault the charger reports a fault
7 0x80 - reserved, 0

The existing EVENTs mark the transitions: charging when bit 1 sets, charged when bit 2 sets, and low_battery before a board shuts down for a low cell.

2.7 Calibration (CAL_SET, CAL_DATA)

The payload is CBOR (RFC 8949) with this profile: definite lengths only; integers in their shortest form; non-integral numbers as float32 when that round-trips, else float64; map keys are text strings and are the keys of the body file's calibration section (imu, display, motors, servos, and inside them accel_offset, gyro_offset, rotation, offset_px, deadband, reverse, ticks_per_rev, min_us, center_us, max_us); map keys are sorted by byte length then bytewise (RFC 8949 4.2.1) so the encoding is deterministic. CAL_SET may carry any subset of the tree; the board merges it over its stored calibration and persists it. CAL_DATA carries the full stored tree. A calibration payload that does not fit one frame is not allowed; the tree is small by construction.

Deprecated in revision 1.1: before PAIR_CHALLENGE and PROVISION existed, hosts carried pairing and network configuration as extra top-level CAL_SET keys pairing and net (section 2.9). A board must still accept them in revisions 1.1 and 1.2 and must never echo net in CAL_DATA; revision 1.3 removes them and answers them with ERR 2 (the removal planned for 1.2 moved to 1.3 when 1.2 became the power-reporting revision).

2.8 Chunked transfers

CMD_AUDIO_PLAY, AUDIO_IN, and SKILL_LOAD split long data into chunks. chunk_idx counts from 0. Audio chunks carry 256 samples (512 bytes, 16 ms) except the last; total in CMD_AUDIO_PLAY is the chunk count of the clip. AUDIO_IN has no total because capture is open-ended; a chunk shorter than 256 samples ends the capture. SKILL_LOAD chunks carry up to 1020 bytes of the rule bundle (rules.json, UTF-8); the board answers each chunk with SKILL_STATUS and a chunk out of order with ERR 6. skill_crc is the CRC-16 (section 1.1) of the whole bundle.

SKILL_STATUS.state: 0 none, 1 loading, 2 loaded, 3 running, 4 error.

2.9 Pairing and provisioning

Pairing needs an identity; provisioning needs a radio. PAIR_CHALLENGE / PAIR_RESULT exist for boards with an identity role (Isaac): a secp256k1 key generated on first boot and never exported, whose address is the device key the RobotIdentity contract pairs with the owner (build document 12.4). PROVISION / PROVISION_ACK exist for boards with a network transport to configure (wifi-mqtt, or ble as a provisioning path), whether or not they have an identity. A board answers a host-to-board type it does not support with ERR 8 (UNSUPPORTED). So Isaac accepts all four; a generated ESP32 scaffold for a transport: wifi-mqtt board accepts PROVISION (answering PROVISION_ACK) and answers PAIR_CHALLENGE with ERR 8; a usb-serial scaffold, and the Python scaffold, answer both with ERR 8.

PAIR_CHALLENGE (host to board), 114 bytes, fixed layout:

offset  size  field
0       32    robot_id    bytes32 of the PairIntent, as on chain
32      20    owner       the owner address (20 bytes, no padding)
52      32    nonce       uint256 nonce of the PairIntent, big-endian: the exact 32-byte word the EIP-712
                          struct hash encodes (RobotIdentity.deviceNonce(deviceKey))
84      8     chain_id    u64, little-endian like every integer in this protocol
92      20    contract    the RobotIdentity address (EIP-712 verifyingContract)
112     2     timeout_s   u16 seconds the board waits for consent; 0 means the default, 60

The board computes the EIP-712 digest itself from these fields (domain RobotIdentity version 1, chain_id, contract; type PairIntent(bytes32 robotId,address owner,uint256 nonce), as @isaac/contracts-sdk hashPairIntent), so it signs only what it has seen. It shows the pairing prompt (the eyes pulse; the host need not send CMD_DISPLAY PAIR_PROMPT any more, though it may) and waits for consent: a talk-button press within timeout_s signs; a press of button A or B rejects. A board whose challenge is still pending answers a second PAIR_CHALLENGE with ERR 5 (BUSY); a frame shorter than 114 bytes gets ERR 2.

PAIR_RESULT (board to host), 118 bytes, fixed layout, one per challenge:

offset  size  field
0       20    device_key  the board's address, always filled when the board has an identity
20      65    signature   r (32), s (32), v (1; 27 or 28) over the digest, recoverable, low-s; all zero
                          unless status is 0
85      32    digest      the EIP-712 digest the board computed, so the host can check its own
117     1     status      0 SIGNED, 1 REJECTED (A or B pressed), 2 TIMEOUT, 3 BUSY, 4 NO_IDENTITY

The host verifies that signature recovers device_key over digest and that digest equals its own hashPairIntent, then submits the pairing on chain. On status 0 the board stores robot_id and emits EVENT paired with no args for its rule runtime; the host confirms the on-chain result later with CMD_DISPLAY NOTIFY paired. Revision 1.0 firmware appended the 20-byte device key and the 65-byte signature as trailing args of EVENT paired instead; hosts may still accept that in revisions 1.1 and 1.2 and must ignore the args from revision 1.3.

PROVISION (host to board): the payload is CBOR in the section 2.7 profile, one map with text keys:

key type meaning
ssid text required; Wi-Fi network
psk text passphrase, empty for an open network
api_base text required; https://host[:port] of the Isaac API
voice_base text base URL of the voice service; defaults to api_base
device_token text bearer token the board sends to the API and voice service
mqtt_host text the agent's broker address (section 3.2); empty means no MQTT link
mqtt_port unsigned int defaults to 1883
voice_ca text PEM of the CA that signs the voice service certificate (see below)

Keys are snake_case like the calibration keys. Unknown keys are ignored or stored as they are (never rejected) so later revisions can add fields. The board merges the map over its stored configuration, so a later PROVISION that leaves out voice_base or voice_ca keeps the stored value; it persists the result in non-volatile memory before it acknowledges, then applies it (reconnects Wi-Fi), and never echoes it: psk, device_token and voice_ca do not appear in CAL_DATA, LOG, or any other frame. Holding buttons A and B for 5 s erases the stored configuration (not the identity key).

Size: the whole map travels in one frame, so its CBOR encoding must fit the 1024-byte payload limit (section 1); there is no chunked PROVISION. A host checks the encoded size before sending and refuses a larger configuration rather than splitting it. In practice that bounds voice_ca: an ECDSA root certificate (P-256 or P-384, about 800 bytes of PEM) fits next to the other keys, an RSA root (about 1.9 kB) does not, and a board meant for an RSA-rooted voice service carries that CA compiled into its firmware instead.

Voice TLS: a board that calls an https voice_base verifies the server against voice_ca when it was provisioned, else against a CA bundle compiled into its firmware; with neither it refuses the call (and reports it with LOG level 3) rather than connecting unverified. A development build may disable verification explicitly; production firmware must not. A plain http voice_base (LAN development) is unaffected.

PROVISION_ACK (board to host): u8 status, char[] text; status 0 STORED, 1 BAD_CONFIG (text names the offending key: map when the payload is not a CBOR map, else ssid or api_base when missing or not text; nothing is stored), 2 STORE_FAILED (the non-volatile write failed; text may name the store). The board answers every PROVISION with exactly one PROVISION_ACK within 1 s, before it reconnects.

2.10 LOG, NACK, ERR

LOG.level: 0 debug, 1 info, 2 warn, 3 error. text is UTF-8 to the end of the payload.

NACK names the seq of a frame that failed its CRC and goes back on the link the bad frame arrived on. A sender may retransmit frames it still holds (CMD_*, CAL_SET, SKILL_LOAD); streamed frames (SENSE, ENC, AUDIO_*) are not retransmitted.

ERR codes:

code name when
1 BAD_TYPE unknown message type
2 BAD_LENGTH payload does not fit the type's layout
3 BAD_INDEX actuator or sensor index out of range for this body
4 BAD_MODE the command is not allowed in the current mode
5 BUSY a previous transfer or calibration step is still running
6 CHUNK_ORDER chunk_idx is not the expected one
7 BAD_CRC a transferred bundle's CRC does not match
8 UNSUPPORTED this board has no such peripheral or feature
9 INTERNAL firmware fault
10 TOO_LARGE a transfer does not fit the board's buffer

text is a short UTF-8 explanation and may be empty.

3. Transports

3.1 USB serial

921600 baud, 8N1, no flow control. The byte stream is framed as in section 1 with no extra delimiters. The agent opens the port, sends HELLO_ACK on receiving HELLO, and sends PING every 250 ms while connected. A board that uses native USB CDC (ESP32-S3, ESP32-C3) ignores the baud rate.

3.2 MQTT (wifi-mqtt)

Topics isaac/<robotId>/up (board to host) and isaac/<robotId>/down (host to board), where robotId is the 0x-prefixed lowercase hex robot id, or the board's body_hash before the robot is registered. The MQTT message payload is exactly one frame, including magic and CRC, so the same stream decoder serves every transport; a board runs one decoder instance per transport so a partial USB frame never mixes with an MQTT message. A new MQTT session restarts the HELLO handshake on that link (section 2.1). QoS 0, no retain. The agent runs an embedded broker on port 1883; a board connects to the agent host's LAN address, which the agent shows during provisioning.

3.3 BLE

Nordic UART Service: service 6E400001-B5A3-F393-E0A9-E50E24DCCA9E, RX characteristic 6E400002-... (host writes, write-without-response), TX characteristic 6E400003-... (board notifies). A frame is split into consecutive writes or notifications of at most 244 bytes; the receiver concatenates them and runs the section 1 stream decoder. Frames are not interleaved on one characteristic. Isaac uses BLE for provisioning and pairing only.

3.4 UART board-to-board and index remapping

A secondary board links to the primary over a uart bus with peer set in the body file, at the bus's baud (default 921600). The secondary speaks this protocol to the primary with its own local indexes: its actuators and sensors numbered from 0 in body file order, restricted to entries whose board is the secondary. The primary relays:

  • Secondary to host: the primary decodes each frame from the secondary, rewrites every actuator_idx and sensor_idx from the secondary's local numbering to the global numbering, and re-encodes it with its own seq. HELLO from a secondary is consumed by the primary and reported to the host as a LOG line.
  • Host to secondary: the primary splits CMD_ACTUATOR by board, sends each secondary the entries that belong to it with local indexes, and executes its own. CMD_STOP, CMD_MODE, and PING are forwarded to every secondary.

Global numbering is the position in the body file arrays over all boards. The firmware generator emits the remap table (indexMaps(body).boards[<id>] in packages/protocol) for the primary.

4. Timing

what rate or limit
SENSE 50 Hz for mobile/1, 20 Hz for companion/1 by default; firmware-configurable
ENC 100 Hz when any actuator has an encoder
CMD_ACTUATOR up to 50 Hz from the host
PING every 250 ms from the host while connected
HELLO every 2 s from the board until HELLO_ACK
safety stop in mode 1 (teleop), 500 ms without CMD_ACTUATOR or PING executes CMD_STOP and the board stays in mode 1
partial frame timeout 200 ms without bytes discards the partial frame (section 1.2)
host runtime stop the host sends CMD_STOP and pauses a skill after 300 ms without SENSE
pairing consent timeout_s of PAIR_CHALLENGE (default 60 s), then PAIR_RESULT status 2 (section 2.9)
provisioning ack PROVISION_ACK within 1 s of PROVISION

Time stamps in frames are board time; the host records its own receive time alongside each frame in recordings (section 5).

5. Recording format (frames.bin)

A session recording (build document 11.3) stores frames exactly as sent or received:

record := u32 host_t_ms, frame
file   := record*

host_t_ms is the host's monotonic time in milliseconds at which the frame was received (board to host) or sent (host to board), relative to the session start. Frames are complete section 1 frames with magic and CRC, so a reader validates each one. Direction is implied by the message type; for the four types that flow both ways (PING, PONG, NACK, ERR) a recording keeps whatever was on the wire and replay tools treat them as informational. A record with a bad CRC or a length past the end of the file makes the recording invalid; recordings are never repaired.

6. Version

This is protocol version 1, document revision 1.2. A change that alters a layout bumps proto_ver in HELLO/HELLO_ACK; a host that receives a higher version than it supports answers ERR 8 and does not proceed. New message types and trailing payload fields may be added without a bump; such additions bump the document revision (the number after the dot) and are listed here. A board that does not know a new type answers it with ERR 1 (BAD_TYPE), which is how a host discovers that it talks to older firmware. Wording that only states what implementations already do on the wire (the per-link HELLO rule, which boards accept PROVISION, the voice_ca key, which older boards keep or ignore as an unknown key, and the one-frame limit on PROVISION) keeps the revision.

revision change
1.0 initial
1.1 PAIR_CHALLENGE, PAIR_RESULT, PROVISION, PROVISION_ACK (0x60..0x63, section 2.9); CAL_SET pairing / net and EVENT paired args deprecated
1.2 HELLO trailing module_variant and hw_revision (section 2.1); power roles battery_voltage, charge_rate, charger with the charger state bits, and the pack temperature sent only while valid (section 2.6)