# Modbus TCP Quickstart Guide

Supplement to the [Modbus TCP register documentation](/modbus-tcp/documentation).
It covers what the station expects from an EMS before charging control over
Modbus TCP behaves as expected, and the write sequences we test against.
Matches the firmware behavior as of July 2026.

## Addressing

Register addresses appear in two notations, and Modbus tools disagree about
which one they expect. This guide always gives both:

* the register number (40202, 30027, ...) - the conventional 1-based notation
  used in the register tables, and
* the PDU address - the value that actually goes on the wire: register number
  minus 40001 for holding registers, minus 30001 for input registers.

Holding registers (4xxxx) are read with FC03 and written with FC06/FC16.
Input registers (3xxxx) are read-only, FC04. If your tool shows an off-by-one,
it is almost always mixing up these two notations. A value that looks legal
but is rejected is usually the other client-side mismatch - see Value
encoding below.

Registers used in this guide:

| Name | Register | PDU (dec) | PDU (hex) | Access |
| --- | --- | --- | --- | --- |
| ChargeMode | 40107 | 106 | 0x006A | FC03 read, FC06/16 write |
| Set_Charge_Current_Target | 40202 | 201 | 0x00C9 | FC03 read, FC06/16 write |
| Start_Stop_Charging | 40203 | 202 | 0x00CA | FC03 read, FC06/16 write |
| Failsafe_Timeout | 40204 | 203 | 0x00CB | FC03 read, FC06/16 write |
| Failsafe_Current_Target | 40206 | 205 | 0x00CD | FC03 read, FC06/16 write |
| Heartbeat | 40208 | 207 | 0x00CF | FC06/16 write |
| EVCS_State | 30001 | 0 | 0x0000 | FC04 |
| ErrorCode | 30004 | 3 | 0x0003 | FC04 |
| Failsafe_Active | 30027 | 26 | 0x001A | FC04 |
| DaemonStatus | 30050 | 49 | 0x0031 | FC04 |
| Availability | 30101 | 100 | 0x0064 | FC04 |
| OfferedCurrentLimit | 30105 | 104 | 0x0068 | FC04 |
| EV_Present | 30201 | 200 | 0x00C8 | FC04 |
| Charging | 30202 | 201 | 0x00C9 | FC04 |
| CP_Status | 30203 | 202 | 0x00CA | FC04 |

## Value encoding

Modbus is big-endian and this gateway follows that exactly, in both
directions:

* Byte order: every 16-bit register is transmitted most significant byte
  first. A current target of 10 A goes on the wire as `00 0A`, never `0A 00`.
* Word order: multi-register values carry the high word at the lower register
  address. This matters only for the f32 and u32 telemetry registers - every
  writable control register in this guide is a single u16.

Some clients expose both as per-object settings instead of deriving them from
the protocol. Where that is the case, every object needs both set to
big-endian, and telemetry that decodes correctly is no evidence that the write
objects are set the same way. Word order is harmless for the control
registers, being single-register values; byte order is not. A byte-swapped
value still arrives, silently transformed: a target of 10 arrives as 2560 and
a target of 8 arrives as 2048, both far outside the accepted range, so the
write is rejected with exception 0x03 while the value on your screen looks
perfectly legal.

The signature is that every current target is rejected whatever value you
pick, with 0 the only one that works - a byte swap leaves 0 unchanged. If you
cannot see the setting in your client, send 2560 as the current target: byte
swapped that is 10, so an accepted write and a charger at 10 A confirm the
swap. Disable any scaling or correction table in the client for that test.

## The short version

1. Plug in the vehicle. EV_Present 30201 (PDU 200) must read 1, CP_Status
   30203 (PDU 202) must be 1, 2 or 3 (IEC 61851 states B/C/D).
2. Switch the charger to manual charge mode: write ChargeMode 40107
   (PDU 106) = 2. Do this before writing any target - every mode change
   resets the current limit to the configured maximum.
3. Start charging with one FC16 frame that writes Set_Charge_Current_Target
   40202 (PDU 201) = 6..32 A and Start_Stop_Charging 40203 (PDU 202) = 1
   together:

   ```
   10 00 C9 00 02 04 00 10 00 01     (16 A + start)
   ```

4. Keep the session alive. The failsafe watchdog is enabled by default with a
   20 s timeout, so write Heartbeat 40208 (PDU 207, any value) at least every
   10 s, or rewrite 40202 at that rate.
5. To change the current, write 40202 again. The charger follows within a few
   seconds; the telemetry registers update at up to 5 s intervals.
6. To stop, write 40203 = 0 (or 40202 = 0).

Send requests to the configured Modbus unit ID (default 1; visible in the web
UI). On a master with linked slave boxes, each slave charger answers on its
own unit ID behind the master's IP address.

## Manual mode, and why the other modes fight you

The Salia has three charge modes: eco (0), power (1) and manual (2). Only
manual hands the current limit over to an external controller.

In eco and power mode the internal charge control runs every 3 seconds
(`charge_control_interval` in the station configuration, default 3.0) and
writes its own result to the same limit channel the Modbus gateway writes to:

* power mode raises the limit by roughly 1 A per tick until it reaches the
  configured maximum. While a vehicle is connected but not yet charging, it
  sets the limit straight to the maximum on every tick.
* eco mode moves the limit up or down by 1 A per tick, following the solar
  surplus. The wandering current in eco mode is the intended behavior, not a
  fault.

A target written over Modbus therefore survives at most until the next tick.
In manual mode the internal control leaves the limit channel alone and the
Modbus value stays in force.

Related points worth knowing:

* The mode is persisted by the charger, so 40107 = 2 survives reboots. Solar
  and eco logic stay off until somebody switches the mode back (web UI or
  register).
* Every mode change resets the current limit: switching to power or manual
  sets it to the configured maximum, switching to eco during an active charge
  sets it to the minimum. Write the mode first, then the target - and after
  any later mode change, write 40202 again.
* Switching to power mode also disables Eco+ and, on stations with phase
  switching fitted, forces 3-phase operation.
* The authorization mode (free / RFID / OCPP / key-switch) is a station
  setting; there is no Modbus register for it (register 30022 / PDU 21 only
  reports it). On stations run against an OCPP backend the backend can
  authorize/deauthorize sessions and impose its own limits on the same
  charger, so for integration tests it is easier to set the authorization
  mode to "free" in the web UI. A Modbus start (40203 = 1) sets the session
  authorization on the station by itself.

## Other things that can move the current, in any mode

These act even in manual mode. They are intentional and not Modbus errors:

* Mains fuse protection. On installations with a mains meter, the charge
  control lowers the limit or pauses charging when the building supply runs
  into its fuse limit. When such a pause is lifted, the limit is set back to
  the configured maximum - write 40202 again afterwards.
* Load management and grid limit. Linked chargers share a current budget, and
  the grid operator limit sits on its own channel. The current actually
  offered to the vehicle is bounded by the lowest active channel, so a second
  charging vehicle or a grid constraint can pull the delivered current below
  your target at any time. Requesting 32 A on a station limited to 16 A
  yields 16 A - that is a clamp, not an error.

## The old heartbeat flag (not the Modbus failsafe)

The Salia also has an older, separate heartbeat feature: the `heartbeat` flag
in the station configuration (saliaconf), toggled over the MQTT topic
`port0/salia/heartbeat`. When this flag is set and no message has arrived on
that topic for more than 60 s, the charge control forces the current limit to
the configured minimum on every 3 s tick while a vehicle is charging - in
every charge mode, manual included. The Modbus gateway never publishes that
topic.

On a Modbus-controlled station this flag must be false. We have seen stations
in the field with it enabled; the telltale sign is the current dipping to
exactly the station minimum (typically 6 A) every few seconds.

## Checklist before debugging registers

| Check | Where | Expected |
| --- | --- | --- |
| Vehicle plugged | 30201 / PDU 200, 30203 / PDU 202 | 1; CP in B/C/D |
| Station operative | 30101 / PDU 100 | 1 |
| No active fault | 30004 / PDU 3 | 0 (7 means failsafe is active) |
| Charge mode | 40107 / PDU 106 | 2 (manual) |
| Charging commanded | 40203 / PDU 202 | 1 (resets to 0 on restart) |
| Current target | 40202 / PDU 201 | 6..32 (0 = stop) |
| Daemon ready | 30050 / PDU 49, bit 0 | 1 (requests during startup return 0x06) |
| Old heartbeat flag off | saliaconf `heartbeat` | false (web/service access) |

## Pitfalls

* Writing 40202 alone does nothing while 40203 = 0. The gateway then commands
  0 A to the charger no matter what target you wrote. Always start with the
  40202 + 40203 pair in one FC16 frame.
* 40202 and 40203 are volatile. After a daemon restart or a reboot of the box
  they are back at 0, and the EMS has to send the start sequence again.
* The failsafe watchdog ships enabled: Failsafe_Timeout 40204 (PDU 203)
  defaults to 20 s, Failsafe_Current_Target 40206 (PDU 205) defaults to 0 A.
  If no accepted control write (40202, 40203, 40208, 40209) arrives within
  the timeout, the charger falls back to the failsafe current, ErrorCode
  30004 reads 7 and Failsafe_Active 30027 (PDU 26) reads 1. The next accepted
  write restores your target. An EMS that writes once a minute will see the
  current bounce between its target and the fallback. Either send heartbeats
  at half the timeout, set 40204/40206 to values that fit your control cycle
  (10..3600 s, 0 or 6..32 A), or disable the watchdog with 40204 = 0. On an
  idle station with no EMS traffic, ErrorCode 7 with Failsafe_Active 1 is the
  normal resting state, not a defect.
* The target is a request, not a guarantee. See the limits section above; read
  OfferedCurrentLimit 30105 (PDU 104), CurrentOffered 30106+ and the meter
  currents 31009+ (PDU 1008+) to see what is actually offered and drawn.
* Set_Charge_Power_Target 40201 (PDU 200) is not implemented. Writes are
  accepted and stored but have no effect. Use the current target 40202.
* 30202 ("Charging", PDU 201, FC04) is a read-only status flag and cannot be
  written. It is easy to confuse with 40202 - same trailing digits, different
  register space.
* The 40107 read-back can go stale: it is set at daemon start and by EMS
  writes, but a mode change made later in the web UI is not mirrored back
  into the register. When in doubt, the web UI shows the mode actually in
  force.

## Symptoms and causes

A useful shortcut: look at the value the current jumps to - it usually tells
you which mechanism is involved.

| Symptom | Likely cause |
| --- | --- |
| Write to 40202 accepted, nothing charges | 40203 = 0, no EV / CP = A, or ErrorCode != 0 |
| Current creeps back toward maximum, ~1 A every 3 s | Charge mode is power -> set 40107 = 2 |
| Current wanders ±1 A around a solar-driven value | Charge mode is eco (by design) -> manual for EMS control |
| Current drops to the failsafe value (default 0 A) ~20 s after the last write, ErrorCode = 7 | Failsafe watchdog tripped -> heartbeat faster, or adjust/disable 40204 |
| Current dips to exactly the station minimum every few seconds | saliaconf `heartbeat` flag is true -> set it to false |
| Current jumps to maximum right after a mode change | Mode-change limit reset -> write 40202 again after changing 40107 |
| Current lower than target while a linked box also charges | Load-management budget split (not an error) |
| Every write to 40202 rejected whatever the value, only 0 accepted | Client byte order is not big-endian -> see Value encoding |
| Exception 0x03 on write | Value outside 0 / 6..32 (station config may narrow the range). If the value you sent is inside the range, suspect the client byte order - a swapped 10 arrives as 2560 |
| Exception 0x04 when starting (40203 = 1) | Current target 40202 is still 0 - write the target first, best both in one FC16 frame |
| Exception 0x06 | Rate limit (50 requests/s per IP) or daemon still starting (check 30050 bit 0) |
| Worked before a reboot, never resumed after | Volatile 40202/40203 are back at 0 -> send the start sequence again |

## Verifying a session

After starting, poll (FC04 unless noted):

* 30001 EVCS_State (PDU 0) = 2 (CHARGING) and 30202 Charging (PDU 201) = 1
* 30004 ErrorCode (PDU 3) = 0 and 30027 Failsafe_Active (PDU 26) = 0
* 30105 OfferedCurrentLimit (PDU 104) and 30106+ CurrentOffered = your target,
  or the station clamp
* 31009+ meter currents (PDU 1008+) = what the vehicle actually draws; the
  vehicle drawing less than offered is normal

If what you see does not match this guide, send us the firmware version
together with the Salia localhost/api values and we will take a look.
