Getting an Iskra Jatena payphone talking again
Initial disclaimer: I've had this blog post in mind for the last three years, but never got around to writing it. Getting an actually competent blog on my page was the final push, so here it is. Some details may be fuzzy; if you have corrections or additional information, please let me know.
In mid-2023, Miha (franga2000) got an Iskra Jatena card payphone from Telekom Slovenije, which we then played around with in Garaža at FRI to get it working. The phone was in good condition, but we had scarce information to go off of. Miha developed a board to interface the display, keypad and handset with an Arduino, so we could "simulate" a payphone. We wanted to get the original electronics working as well, though.
Around June of that year, we visited LF3M, the successor of Iskra Telematika, who made the phone. We came away with two shells, some PCBs and contacts who later helped us get more phones, including the incredibly rare Postel and Hostel models. We also got the payphone's source code, from the last revision dating to the early 2000s. Actual C source, with function names and comments, rather than a ROM dump to disassemble. I set out to write a replacement server.
Finding something to plug it into
First we needed a modem. The faculty had none except a softmodem that wasn't going to cut it, so I got a USRobotics 5630B. We also got a free ATA from Bolha, a Welltech ATA172Plus. It's a rather odd device, but useful for payphones: besides polarity reversal, it supports 12 kHz and 16 kHz metering pulses. The Jatena uses 16 kHz pulses to charge for calls.
Configuring the ATA was a pain because every change triggered a reboot. Eventually we landed on a working configuration with G.711 μ-law and 16 kHz metering, with the jitter settings adjusted as well. The voice codec matters here because the modem has to survive the audio path; getting a normal voice call through doesn't tell you whether a data call will work.
We ran FreePBX on a Dell laptop. That arrangement lasted a couple of years before we moved the PBX onto pve.si.
The phone's modem talks V.21 at 300 baud. On the computer side, my server opens /dev/ttyUSB0 at 9600 baud, which is a separate connection between the computer and the USRobotics. The modem handles the slower telephone-line connection. The initialization in modem.c enables V.21 with ATS27=1 and sets the modem to answer automatically.
Why the phone needs a server
The firmware runs as a state machine, handling events such as inserting a card or receiving a metering pulse. It also watches for faults and tampering, including someone opening the phone or tearing off the handset. These aren't just messages for the display; the phone reports faults to the operator.
Once a day, at a configured time, it calls the OMC, or operations management center, to upload its reports and collect settings. Faults can also trigger a call. At first we had no idea what the original OMC looked like, but the phone's source told us what it expected the other end to do. We got the original OMC source later.
For our purposes, the server had to answer the modem and get through that conversation without leaving the phone waiting for a response it would never receive.
The bytes on the line
The framing slightly reminds me of the Millennium's X.25 communication, but the Jatena has its own format. The relevant code lives in frame.c. A normal first data frame has this shape, before escaping:
01 | request/response | sequence | type type | length_hi length_lo | data | FCC FCC | 03
01 starts the frame and 03 ends it. Request frames use 05, while acknowledgements use 06. The opening identification exchange uses 07, which the code calls XID. Not every frame has the full header above: an acknowledgement is short, and continuation frames don't repeat the transfer length.
The first data frame repeats a one-byte type, such as DD for status or PP for statistics, followed by a two-byte length. That length uses big-endian order, high byte first. Calling the whole payload binary would also be misleading, since the phone mixes ASCII records with binary fields. Statistics, for example, contain a P followed by a two-digit counter number and a decimal value; card serial numbers use binary bytes.
What happens if the data contains 01 or 03? The sender escapes those bytes, along with 1B, by writing 1B followed by the original byte plus 80 hex. So a data byte of 03 travels as 1B 83, rather than accidentally ending the frame.
The two checksum bytes aren't a CRC16. The firmware calls them FCC and calculates them by adding the bytes between the start marker and the checksum, including the escape sequences. It then rearranges the sum into two bytes with their high bits set. That's important when implementing the receiver: calculating a CRC, or adding up the unescaped payload, won't match the phone.
There's another detail my implementation doesn't handle properly throughout: the original protocol alternates sequence bytes between 70 and 71 hex. My server often just increments its frame counter. Enough to work with some of our boards, apparently, but not a good basis for claiming full compatibility.
What actually gets transferred
After the opening exchange, the phone sends its configured identifier and a status report. The server then walks it through the remaining uploads before sending settings back. In the firmware revision we have, the reports include:
| Type | Contents |
|---|---|
D |
Numbered status flags, covering faults and operating conditions |
P |
Usage counters, including calls and charged metering pulses |
U |
Newly encountered card serial numbers, with a card-type byte |
H |
Stored call records, including pulse consumption, card details and the dialled number |
The call table isn't necessarily a record of every call. The firmware uses an SPD setting to control the pulse threshold for saving a number; its default is 25 pulses. Each call record carries the pulse count and card serial number before the telephone number, with FF separating records.
Larger transfers span several frames. The first frame announces the transfer length, while later frames continue the same report. The server has to acknowledge them and keep collecting data until it has the complete transfer, rather than treating every frame as a new report.
My replacement server follows this conversation with another state machine. It saves the uploads as statistics.bin, new_cards.bin and usage.bin, then sends spd.bin and tar.bin back to the phone. Each phone gets a directory under data/, named after its identifier. For input files, the server falls back to data/default/ if the phone doesn't have its own copy.
It's fairly crude. The files let us keep the modem conversation separate from whatever would eventually edit the settings, but the server doesn't turn all those uploads into readable reports. It also has gaps in its packet handling, particularly around escaping and acknowledgements.
Settings, tariffs and card swapping
SPD is the phone's settings data. In spd.c, records start with S and a two-digit setting number; FF separates values within a record. S13, for example, holds four values: the current hour and minute, followed by the hour and minute for the next daily call. The original OMC can insert its current time and the phone's assigned reporting time into that record before sending it.
Quite a lot of the phone's behaviour depends on these settings. They control which card types it accepts and whether it checks for missing charging pulses. Other settings choose pulse or tone dialling, set number-length limits, and control incoming calls or keypad dialling during a conversation. The server can also enable a card-blacklist transfer, although my replacement doesn't implement that transfer.
The tariff download is separate from SPD. The phone follows the digits as you dial to find the appropriate tariff category, which can allow free calls or reject a number. Some categories require a minimum amount of credit before the call can proceed. So sending metering pulses from the ATA only solves part of the problem; the phone still applies its own rules about what you may dial.
The dialling jump table
The actual lookup in t_cona.c is a table with ten columns, one for each digit from 0 to 9. The phone starts at row zero and uses each digit as a column index. An entry below 50 means it has found a category. An entry of 50 or more means "subtract 50 and jump to that row for the next digit." It's a prefix tree stored as an array, rather than a list of complete telephone numbers to search through.
For example, here's how the built-in ROM table handles 112:
Start at row 0
digit 1 -> entry 53 -> continue at row 3
digit 1 -> entry 54 -> continue at row 4
digit 2 -> entry 33 -> category 33
So after the first 1, the phone doesn't know enough yet, and the second 1 sends it to another row. Only the 2 settles it: category 33, which allows a call without a card. The lookup stops there. For an ordinary subscriber number, it can settle on a category before you've finished dialling; it only needs enough digits to decide how to handle the call.
Despite the name, the tariff table also decides what you can't call. Category 38 blocks a prefix, and the phone removes the digit that triggered the restriction from its dialling buffer. The operator could change these rules from the OMC, so the table in ROM only tells us the defaults for this firmware, not what every Jatena allowed in service.
Updating it doesn't involve sending that whole array over a 300-baud connection. Instead, the OMC sends prefixes grouped by category, and the phone builds the rows itself. In the supplied TARXXX1.TAR, for instance, 06, 09 and 05 belong to category 11, while 00 belongs to category 21. Each group starts with A and its two-digit category number, with FF bytes between prefixes. Most of the file is just ASCII digits.
There's a catch when building the table: the order of the rules matters. Adding a longer prefix creates another row, which inherits the old category for all the other digits. But adding the shorter prefix afterwards can overwrite the pointer to that row, losing the more specific rules underneath it. It's something a tariff editor would have to get right before sending the file.
The phone switches to the new table once the OMC session finishes successfully. That lets us change which prefixes it accepts without touching the firmware; the ATA still supplies the metering pulses that charge the card during a call.
Changing cards mid-call
One feature I like is the card-change button. When the remaining balance gets low, the phone can prompt you to insert another card without ending the call. The firmware consumes the old card's remaining credit into a temporary balance, keeps charging against it while you swap cards, and adds the new card's credit when it accepts it. There's a whole state for this in M_aplik.c, including what to do if you insert an empty or invalid card.
Why some boards still don't work
Our server worked with a few boards, but not all of them. The trouble was in the exchange itself: some boards sent double ACKs or otherwise didn't follow the sequence my server expected. There's also a special case in the server for a repeated opening handshake, but that's a separate issue.
Looking at the original source now, there are two kinds of confirmation to keep apart. In the phone's frame.c, ConfirmFrame() sends a short 06 acknowledgement. ConfReqFrame(CONFIRM, SPD) sends a longer command frame saying that the SPD transfer has finished. After the last SPD block, the phone calls both functions, one after the other. It does the same for the last tariff block. That's two responses to one block, though not two identical ACK frames.
The transition from settings to tariffs is particularly fussy. After confirming the SPD transfer, the phone enters its TAR state. The next received frame makes it send a tariff request and enter ATT1; the following frame only moves it from ATT1 to ATT, without reading any tariff data. Only then does it expect the first tariff block. Those cases don't check the incoming frame type before changing state. So at this point, an extra frame can change what the phone expects next.
My states.c has its own assumptions about these replies. Normally it skips short ACKs, but during multi-block downloads it sets PASS_ACK, allowing an ACK to advance the state machine and send the next block. It doesn't check that the ACK's sequence number matches the outstanding block. If two ACKs arrive while that flag remains set, both can advance the transfer. There's also a TAR_REQUEST_INTERIM state that sends an extra ACK, and another ACK before sending the first tariff block. The comment beside the interim one literally says to play around with the number of ACKs.
The original OMC's RS232.PRG separates those jobs. KOM_read() handles short ACKs and releases queued outgoing blocks, while CL_data() handles the longer commands and reports. It also immediately acknowledges incoming 05 request frames, before the application processes them. RC_Psend() queues outgoing messages when a reply is outstanding, and CL_time() resends on timeout. That gives us a better reference for the expected exchange than my fixed sequence of handlers, although the ACK handler there doesn't explicitly reject duplicate sequence numbers either.
So the source does show why "one reply means move to the next state" is too crude. It also shows legitimate pairs of responses that a receiver mustn't confuse with duplicate ACKs. What it doesn't give us is a record of the double ACKs from each of our boards; we'd still need captures to say exactly where those boards diverge. The Z/U and G/H report variants in the OMC are another compatibility detail, but they aren't the ACK problem we ran into.
I also started a Django web interface for the OMC, listed as Jatena Server on the projects tab. It's still in very early development. For now, the working part is the small C server and the phones it can talk to; finishing the interface won't fix the boards that disagree with its protocol handling.
If you want to follow the code, the phone firmware explains what the device expects, while the original OMC shows how Iskra handled the other end. My server sits somewhere between those two, with rather more TODOs.