The Copperline Control Protocol (CCP) is a versioned JSON-RPC 2.0 interface over TCP for programmatic control of the emulator. It allows scripts, developer tools, CI runners, and automated agents to inspect state, set breakpoints, step execution, inject input events, change media, and capture framebuffers.
Starting the control server¶
# Headless mode (server owns execution; paused at reset):
./target/release/copperline --config kick13.example.toml --noaudio \
--control :0 --control-info /tmp/ccp.json
# Windowed mode (attaches control server to interactive session):
./target/release/copperline --config kick13.example.toml --control-gui :7710--control-gui can share the window with --gdb-gui (see GDB):
a machine stop answers whichever resumes are pending on both clients, a stop
the control client did not request arrives as event.stopped, and
reverse_*, memory.last_writer, mouse.to, and state.load are refused
while the GDB client’s continue is outstanding (pause first). Either
client’s pause ends the other’s run with reason pause.
When --control-info FILE is specified, connection details are written to a
JSON file:
{"listen": "127.0.0.1:52114", "token": "1f0c...", "proto": 1}With --run PROG, either mode arms a one-shot loadseg stop for the
program before the machine runs, the break-at-entry --gdb has: the first
continue (or, windowed, the boot already under way) stops with reason
loadseg the moment the guest OS loads the program, before its first
instruction, and segments.list then reports its hunks. A windowed session
that reaches the stop before any client attached parks there and tells the
first client with event.stopped. The stop fires once; a client wanting
every load arms its own loadseg break.
Client usage (copperline-ctl)¶
Copperline includes a CLI tool (copperline-ctl) to interact with active control sessions:
# Query status
copperline-ctl --info /tmp/ccp.json status
# Add a PC breakpoint
copperline-ctl --info /tmp/ccp.json break.add '{"kind": "pc", "addr": "0xFC0100"}'
# Resume execution (blocks until a breakpoint or stop event occurs)
copperline-ctl --info /tmp/ccp.json continue
# Interactive REPL session
copperline-ctl --info /tmp/ccp.json --repl
# Describe a save state (no session needed) and write out its thumbnail
copperline-ctl state-info /tmp/at120.clstate --thumbnail /tmp/at120.pngA/B divergence finder¶
copperline-ctl diverge launches two headless sessions (two builds, or one
build under two configs) and drives them in lockstep over this protocol,
comparing the frame digest, the CPU registers and a server-side RAM digest
at every frame, then narrowing the first mismatch to the instruction and,
for memory, to the byte. See A/B divergence finder.
Debug adapter¶
copperline-ctl --dap serves the Debug Adapter Protocol over the
same bridge: an IDE debugs a program in the emulator with source-level
breakpoints, variables and reverse stepping, while the control protocol
underneath stays available from the Debug Console (!status, !beam.get).
MCP server¶
copperline-ctl --mcp exposes the control protocol over standard I/O as a
Model Context Protocol server. This allows AI
coding agents in environments like Claude Code or Cursor to drive and inspect the
emulator directly via structured tool calls. Protocol methods map directly to
MCP tools alongside session management utilities.
# Unattached: the agent launches or attaches a session with session tools.
copperline-ctl --mcp
# Attached at startup to a running control server:
copperline-ctl --mcp --info /tmp/ccp.json
copperline-ctl --mcp --connect 127.0.0.1:7710 --token HEXClaude Code registers it with one command:
claude mcp add copperline -- copperline-ctl --mcpor via .mcp.json in a project:
{
"mcpServers": {
"copperline": {
"command": "copperline-ctl",
"args": ["--mcp"],
"env": {"COPPERLINE_BIN": "/path/to/copperline"}
}
}
}initialize returns an instructions summary of the workflow, and
tools/list provides descriptions, JSON schemas, and parameter conventions
for all tools.
Tool names¶
MCP tool names support [a-zA-Z0-9_-], so protocol methods map to tool names
with dots replaced by underscores (e.g. warp.get becomes warp_get,
media.floppy.insert becomes media_floppy_insert, and capture.screenshot
becomes capture_screenshot). Methods without dots (such as status or
run_until) retain their exact names. Tool arguments correspond to method
parameters (addresses accept hex strings or integers). Protocol errors return
with isError: true containing the error code and message. Connection
handshake methods (hello, auth) are handled internally by the bridge and
not exposed as tools.
Session tools¶
The bridge manages one active session at a time:
session_launch {"config", "model", "run", "whdload", "factory", "args", "binary", "cwd", "timeout_ms"}: Spawns a headless emulator instance (copperline --control :0 --control-info TMP --noaudio) with optional configuration flags, connects, and authenticates. Output is redirected to a temporary log file. The instance starts paused at reset.session_attach {"info_file"}orsession_attach {"listen", "token"}: Attaches to an already running--controlor--control-guiserver.session_status: Reports connection state, endpoint address, process ID and log path of any launched emulator, and event queue statistics.session_close: Disconnects from the server and terminates any emulator process launched by the bridge (sending SIGKILL after a 3-second grace period). Closing standard input automatically closes the session.
Blocking and wait_ms¶
Execution methods (continue, run_until, step, step_over, step_out,
step_copper, step_frame) accept an optional wait_ms parameter. If the
emulated machine does not halt within this time limit (in host milliseconds),
the bridge automatically pauses execution and returns the stop event with
bridge.paused_after_ms set. Without wait_ms, calls block until a breakpoint
or stop condition is reached.
Events¶
An internal reader thread maintains an event queue (up to 1,024 items) to
collect asynchronous events during execution. Use events_next to retrieve
individual events with a timeout or events_drain to retrieve all queued
notifications. Both report queue depth and dropped event counts.
Screenshots¶
capture_screenshot returns the PNG image as an MCP image content block
(image/png) alongside text output. If path is omitted, a temporary file is
used and deleted after reading. When path is provided, the image is saved to
that path (relative paths resolve against copperline-ctl’s working directory).
Protocol subset¶
MCP 2025-06-18 over stdio (newline-delimited JSON-RPC 2.0): supports
initialize, notifications/initialized, ping, tools/list, and
tools/call. Invalid requests return standard JSON-RPC error codes (-32600,
-32700, -32601). Standard output is reserved strictly for protocol
messages; diagnostic logs are sent to stderr. The server exits upon EOF on
stdin.
Protocol overview¶
Wire format: Newline-delimited JSON-RPC 2.0 over TCP.
Authentication: Clients authenticate upon connection by sending
hello {"token": "..."}orauth {"token": "..."}.Numbers and addresses: Numeric parameters accept integer values or hex strings (e.g.,
"0xDFF096"or14676118).Execution commands: Commands such as
continue,step, andrun_untilblock until execution stops, returning a structured stop event.
Example stop event payload¶
{
"reason": "breakpoint",
"detail": "Breakpoint at $FC0100",
"pc": 16515328,
"frame": 122,
"vpos": 44,
"hpos": 101,
"cck": 8712345,
"seconds": 2.456,
"retired_instructions": 1745210
}Streaming observability¶
An authenticated client can subscribe to asynchronous event notifications:
events.subscribe {"events":["frame","serial","interrupt","media","debug","bus"],"frame_interval":50,"frame_digest":true}
events.list
events.unsubscribe {"events":["serial"]}Event types¶
event.frame: Emitted per video frame (or perframe_interval). Includes timeline position and optional FNV-1a framebuffer hash digest, plusguest_idle_cck: the colour clocks the guest declared idle during the last frame through the uaelib trap’s idle markers (null until it uses them).event.serial: Emitted when Paula serial transmission occurs.event.interrupt: Emitted when interrupt request and enable state transitions occur.event.media: Emitted when floppy disks or CD images are inserted or ejected.event.debug: Guest debug output through the uaelib trap: one notification per item, withkindlog(text, aKPrintFline, also echoed on the host console) orresource(actionand the registeredresource, asdebug.resourcesreports it).dropped_eventscounts items the bounded queue lost before this batch.event.bus: A named hardware edge from the exact chip-bus timeline, including blitter start/final-D/finish/IRQ, Copper wake/denial/SKIP, CPU interrupt and STOP edges, INTREQ, and CIA IRQ pins. Each notification carries the raweventsmask, decodedevent_names, beam/timelineposition,ipl, and queue drop count. Subscribing does not allocate a full frame trace.event.warp: Sent without a subscription, in both modes, whenever warp or its holder set changes for a reason other than the client’s ownwarp.set:{"on", "paced", "source", "position"}withsourceone ofmanual,guest,gdb,launch,boot,power_off. A windowed session addsholders, every programmatic hold still in force (control,gdb,guest), and also sends the event when a holder joins or leaves without pacing changing, so the list never goes stale. The headless server has no holds (it is unpaced end to end), omitsholders, and reports the guest’swarpmode()request withpacedalways false.
Command reference summary¶
Session management¶
hello {"token": "..."}: Handshake and protocol version query.auth {"token": "..."}: Authenticate active connection.status: Returns emulation state, frame counters, host execution timing, and pacing (paced,warp).shutdown: Terminates the emulator process.
Execution control¶
continue: Resume execution.step {"n": 1}: Single-step CPU instructions. A CPU parked inSTOPretires nothing until an interrupt arrives, so a step there carries it to the one that wakes it and retires the handler’s first instruction; when no interrupt can reach it (SR mask 7, or nothing enabled inINTENA) the step gives up after two video frames and the stop event reports the unchangedretired_instructions.step_over: Step over subroutine call.step_out: Step out of current subroutine.step_copper: Step single Copper instruction.step_frame {"n": 1}: Step video frames.run_until {"pc" | "pc_outside" | "vpos" | "frame" | "cck" | "seconds" | "stable_frames"}: Run until condition.pc_outsideis[LOW,HIGH], ortruefor the default Kickstart window$F80000-$FFFFFF.pause: Pause active execution.machine.reset {"kind": "warm"|"cold"}: Reset the emulated machine (default: warm).
Speed¶
warp.get: Report whether warp (unpaced emulation) is active, whether the machine is paced, and the source holding warp (none,manual,control,gdb,guest,launch,boot,capture, orheadless); in a windowed sessionholderslists every programmatic hold in force (control,gdb,guest),sourcebeing the first (the headless server has no holds and omits the field).warp.set {"on": true|false}: Engage or release the client’s own warp hold (unthrottled execution with audio muted). Holds are independent: releasing yours re-paces the machine only when no other holder (a GDB client, the guest) remains, and the reply’snotesays who still holds it. Disabling warp also cancels active--runor--warp-bootphases.
Reverse execution¶
reverse_step {"n": 1}: Step backward by instruction.reverse_frame: Step backward by video frame.reverse_continue: Execute backward to previous breakpoint.reverse_anchor: Snapshot the machine into the reverse-debug ring at the current position, so the reverse verbs replay from here rather than from an older frame boundary. Take one at a stop to step back from when the guest has used a host directory mount or a disk image since the last snapshot: that host-side state is not rolled back by a restore, and a replay from before it diverges (the DAP adapter does this at every run stop).last_writer {"addr": "..."}: Find the instruction that last wrote to memory address.
State inspection and modification¶
regs.get/regs.set {"reg": "...", "value": ...}: Read or modify 68k registers.regs.getincludes exact raw FP0-FP7 plus FPCR/FPSR/FPIAR when an FPU is fitted.mem.read {"addr": ..., "len": ..., "encoding": "hex"|"base64"}/mem.write {"addr": ..., "data": "...", "encoding": "hex"|"base64"}: Read or modify memory.mem.digest {"region": "chip"|"all"}ormem.digest {"addr": ..., "len": ...}: FNV-1a digest of RAM computed inside the emulator, for change detection and for comparing two sessions without moving the bytes.chip(the default) digests the fitted chip RAM bank,allevery writable RAM bank (chip, slow, motherboard, accelerator, Zorro boards) one by one;addrandlendigest one span through the CPU map instead (a span that lies inside one bank is hashed in place, anything else is peeked byte by byte, at most 256 MB). The reply carriesdigestover the whole andregions, each bank’sbase,lenanddigest. Allowed in a resume verb’scollectlist.disasm {"addr": ..., "count": ...}: Disassemble instructions at address (default: PC). Every line includescycles_minandcycles_max, evaluated through the selected 68000-family core’s generation-specific timing path. These are theoretical CPU cycles; precise profiles additionally measure bus contention.symbols.resolve {"addr": ...}: Resolve one address against the running guest’s library/device jump targets and ROM resident modules. The result reportsfoundand, when found, the symbol’s start, module, name, offset, kind (lvoorresident), and vector/LVO metadata.symbols.rom: Snapshot the actual ROM ranges, resident tags, and named live LVO targets. It walks Exec and reads eachJMP abs.l, so patched entries and every Kickstart/AROS revision use their current addresses rather than a per-ROM database.custom.read {"reg": ...}/custom.dump: Query custom chipset registers.custom.dump.regsis the compact name/value map;registersadds offset, access direction, chipset availability, summary, and the shared Markdown register page.custom.writer {"reg": ...}: Query last PC and beam cycle that wrote to custom register.palette.dump {"resource": ...}: Query active 32-color or 256-color palette; withresource, read a guest-registered palette resource (wordsas 12-bit values plusrgb24).cia.get {"cia": "a"|"b"}: Query CIA-A or CIA-B timer, port, and interrupt states.beam.get: Query raster beam coordinates (VPOS, HPOS, colour clock).frame.slots {"row": V}: Return the bounded full records for one scanline (row 0 through 2047, covering the ECS 11-bit programmable vertical range) of the latest full Frame Analyzer/profile trace. Each entry has HPOS, register, address, data/size, kind/subtype, flags, IPL, raw event bits, and decoded event names.datais a fixed-width hexadecimal string so grouped 64-bit AGA fetches remain lossless in JSON. Owner-only traces and out-of-range rows return errors.instantaneous_recordscontains any ordered zero-time floppy-turbo transfers at positions on the requested row; replay these after the ordinary record at the matching HPOS.blit.render {"index": N, "channel": "A"|"B"|"C"|"D"|"result", "path": "..."}: Reconstruct a recorded blit channel from the exact DMA word stream and write it as PNG. The reply includes the selected plane count and whether it came from a registered bitmap containing BLTDPT or the frame’s BPLCON0, plus the safely decodedrender_planes, interleaving status, and simplified Boolean minterm formula. Non-interleaved resources and the BPLCON0 fallback render one plane rather than guessing that consecutive blit rows are planes of one image. Disabled A/B/C channels use their effective constant input (including BLTBDAT’s write-time-shifted hold latch).resultuses captured D writes and returns an error when no destination stream exists rather than approximating the hardware shift/mask/fill pipeline. The path is optional.display.get: Query active display parameters, viewport size, and pixel format.rtc.get/rtc.set {"unix": ..., "time": "...", "advance": ..., "frozen": ...}: Inspect or move real-time clock.clipboard.get/clipboard.set {"text": "..."}: The host <-> guest clipboard bridge ([clipboard] share,--clipboard; see Configuration).getreports whether the unit is fitted and sharing, whether the guest bridge is up, the host and guest text generations, and the newest text the guest copied.setstages text for the guest exactly as a windowed session’s host clipboard poll would, so a headless run (which never reads the host clipboard itself) can hand the guest text to paste;replay_unsafeis set when reverse execution is armed, as formem.write.cartridge.get: Query freezer cartridge state (model, memorybase/size, monitorversion,enteredstatus,nmi_pending, and freeze count).cartridge.freeze: Trigger the freezer cartridge NMI (level 7), transferring execution to the monitor.copper.list {"addr": ..., "resource": ..., "max": ..., "trace": true}: Disassemble Copper instructions starting ataddror a registeredresource(default: current Copper PC). Withtrace, each instruction that ran in the last full Frame Analyzer trace carries its exact frame, VPOS and HPOS execution slot.pc_history: Return recently executed instruction addresses.segments.list: The hunk segments (current:{start, size}per hunk, first hunk first) of the program the scheduled process is running, and every program an armedloadsegcatch has seen loaded (modules). At aloadsegstop,currentis the just-loaded program: the addresses to relocate its symbols and debug information by.
Windowed UI¶
ui.show {"window": "debugger"|"console"|"analyzer"}: Open or focus one of the inspectors in the main window’s Debug layout. This is available only from a windowed--control-guisession; a headless server returns an unsupported error.
Diagnostics and profiling¶
chipset.validate {"enabled": ..., "clear": ...}/chipset.report: Arm or query custom register access validator.smc.detect {"enabled": ..., "clear": ...}/smc.report: Arm or query self-modifying code detector.fault.inject {"addr": ..., "len": ..., "on": "read"|"write"|"both", "count": ...}: Inject memory bus faults.fault.list/fault.clear: List or clear active memory bus faults.memory.heatmap {"enabled": ..., "base": ..., "span": ...}: Enable or configure address-space access tracking.memory.heatmap.report {"path": "..."}: Export memory access heatmap.debug.resources: List bitmaps, palettes, and copper lists registered by guest software via the uaelib trap.debug.resource.export {"address": ..., "path": "...png"}: Export a registered bitmap or palette through the same decoder as the Resources tab.debug.idle: Query guest idle time statistics reported via uaelib idle markers.trace.start {"path": "...", "max_lines": ...}/trace.stop/trace.status: Control instruction execution trace logging.waveform.start {"path": "...", "trigger": "...", "duration": "...", "signals": "..."}/waveform.stop/waveform.status: Control VCD logic analyzer waveform capture.profile.start {"path": "...", "frames": ..., "slots": ..., "memory": ..., "screenshots": "none"|"every"|"last", "pc_samples": ..., "samples": ..., "registers": ..., "unwind": {"base": ADDR, "table": BASE64}, "relocation_bases": [ADDR, ...], "code_ranges": [{"base": ADDR, "size": N}, ...], "coverage": ..., "trigger": {"frame": F}|{"busy_cck_over": N}}/profile.stop/profile.status: Export per-frame profiling data (DMA ownership, full slot/event records, frame-start custom registers and palette, blit records, CPU chip-bus wait attribution, guest idle time, retired instructions, and stack bounds).memorysnapshots chip and slow RAM once; because that baseline must align with the first recorded frame, it cannot be combined with a deferredtrigger.slotswrites a raw 24-byte-record sidecar per frame.samplesadds a WinUAE/Bartman-compatible per-instruction binary sidecar;registersadds D0-D7/A0-A7/SR, the optional compact unwind table supplies live call stacks,relocation_basespreserves every program hunk’s runtime base for offline source mapping, andcode_rangesidentifies executable hunks outside the compact hunk-0 table.coveragecounts every retired instruction overcode_ranges(every address, bounded, without them) and writes the histogram ascoverage.binat stop forcopperline-ctl profile-report --format lcov; it takesrelocation_bases/code_rangeswithoutsamplesand is refused while a--coveragerun holds the counters. Data streams toprofile.jsonlwith aprofile.jsonsummary upon stop; see Per-frame profiling. Arms Frame Analyzer tracing immediately and begins recording only when an optional trigger matches.
Breakpoints and traps¶
break.add: Add breakpoint (pc,watch,reg_watch,beam,copper,catch,loadseg). A memory watch accepts"access": "write"|"read"|"access"(defaultwrite).break.remove {"id": ...}: Remove breakpoint by ID.break.list: List all active breakpoints.break.clear: Remove all breakpoints.
Input injection¶
input.key {"rawkey": ..., "action": "press"|"release"|"tap", "hold_ms": ..., "at_seconds": ...}: Inject keyboard events.input.type {"text": ..., "at_seconds": ...}: Typetexton the US Amiga keyboard as raw key press/release pairs (Shift where needed, newline as Return, tab as Tab, escape as Esc), one key every 100 ms of emulated time fromat_seconds(default now). Text with characters the US keymap cannot type is rejected. The same conversion backs--type-afterand the window’s Paste as Keystrokes.input.mouse {"dx": ..., "dy": ..., "left": ..., "right": ..., "middle": ..., "port": 1|2, "at_seconds": ...}: Inject mouse motion/buttons.input.mouse_to {"x": ..., "y": ..., "port": 1|2, "tolerance": ..., "max_frames": ...}: Steer pointer to screen pixel coordinates via sprite 0.input.joy {"up": ..., "down": ..., "left": ..., "right": ..., "red": ..., "blue": ..., "green": ..., "yellow": ..., "play": ..., "rwd": ..., "ffw": ..., "port": 1|2|3|4, "at_seconds": ...}: Inject joystick / CD32 button state. Ports 3 and 4 are the parallel-port four-player adapter’s sockets (driving one fits the adapter); on a light penredis the tip switch / trigger.input.analogue {"x": ..., "y": ..., "port": 1|2, "at_seconds": ...}: Set analogue paddle/pot position (0-255).input.pen {"x": ..., "y": ..., "at_seconds": ...}: Hold the light pen over presented pixel (x, y) (theinput.mouse_tocoordinates); omit or negate to lift it off the glass.input.set_port {"port": 1|2|3|4, "device": "mouse"|"gamepad-mouse"|"joystick"|"cd32"|"analogue"|"lightpen"|"none"}: Change port device (ports 3 and 4 takejoystickornone).input.get_ports: Query active controller port device assignments (port1-port4,parallel_adapter, and the light pen’sport, whether it iswiredto Agnus LP, and its position).
Media management¶
media.floppy.insert {"drive": 0, "path": "...", "write_protected": true}: Insert floppy disk image.media.floppy.eject {"drive": 0}: Eject floppy disk.media.floppy.query: Query connected floppy drives, mounted disk images, and write-protection status.media.cd.insert {"path": "..."}: Insert CD image.media.cd.eject: Eject CD image.pcmcia.insert {"card": "cf"|"sram", "path": "...", "size": "2M", "read_only": false}: Push a card into the A600/A1200 PCMCIA slot -- a CompactFlash card over the hard-disk image atpath(the default kind), or an SRAM card ofsizebytes optionally mirrored topath. Any card already in the slot is ejected first; Gayle latches the card-detect change, so the guest sees a real insertion (INT6 once card.resource has enabled it). Fails on a machine without the slot.pcmcia.eject: Pull the card out of the slot (latches the card-detect change the same way).pcmcia.query: Report the slot:slot(the machine has one),enabled(not disabled by the guest or shadowed by Zorro II fast RAM),shadowed_by_fast_ram,inserted,card(cf/sram),description,path, Gayle’s sampledpins, and its pendingchange_latches. Themediaevent stream also reports{"kind": "pcmcia", "action": "inserted"|"ejected", "name": ...}.copperhf.attach {"unit": 0, "path": "...", "volume_name": "...", "boot_pri": 0}: Hot-attach acopperhf.deviceunit’s media (openspathexactly like a boot-time[copperhf]unit,volume_name/boot_prioptional). Bumps the unit’s change counter and sets itsCHF_CHANGED_MASKbit. Fails if no[copperhf]controller is configured.copperhf.eject {"unit": 0}: Hot-eject/detach acopperhf.deviceunit’s media. The unit stays present (CHF_UNIT_PRESENT); only its media bit (CHF_UNIT_MEDIA) clears. Bumps the change counter and setsCHF_CHANGED_MASK, the same as the guest’s ownTD_EJECT.
State snapshot files¶
state.save {"path": "..."}: Snapshot machine state to file. The file carries a metadata chunk (thumbnail of the current frame from the same renderer ascapture.screenshot, emulated and wall-clock save times, machine summary, media names) ahead of the machine.state.load {"path": "..."}: Restore machine state from file.state.info {"path": "...", "thumbnail": "..."}: Describe a state file without loading it -- its containerversion, themachineit was taken on (summary, model, CPU, chipset, video standard, RAM sizes, ROM fingerprint), and itsmeta(emulated_seconds,emulated_frames,saved_at_unixand readablesaved_at,machinesummary,mediawithfloppiesper connected drive,hard_disks, andcd, and thethumbnailsize in pixels and bytes), ormeta: nullfor a state written before the chunk existed. Withthumbnail, the PNG is written to that host path andthumbnail_pathnames it. Reads only the file’s header; the running session is untouched, so it is allowed while the machine runs.
The same card is printed without a session by
copperline-ctl state-info STATE.clstate [--thumbnail FILE.png], as
pretty-printed JSON with the same fields.
Framebuffer capture¶
capture.screenshot {"path": "...", "overlays": ["blits", "overdraw", "sources"]}: Write a PNG from the side-effect-free display renderer. Optional overlays outline recorded blitter destinations, heat pixels by repeated D-channel and other chip-memory writes from a full Phase 2 trace (falling back to the captured D stream without one), and colour final Denise/Lisa output by playfield 1, playfield 2, sprite number, background, or outside-DIW provenance. They work in headless sessions and the MCP bridge returns the resulting PNG as its image block.capture.digest: Return FNV-1a hash digest of current frame.capture.region_digest {"x": ..., "y": ..., "w": ..., "h": ...}: Return hash of screen region.
Streaming events¶
events.subscribe {"events": [...], "frame_interval": ..., "frame_digest": ...}: Subscribe to asynchronous event stream.events.unsubscribe {"events": [...]}: Unsubscribe from events.events.list: List active event subscriptions.