Exemples de code
Trois clients complets, prêts à copier-coller, qui certifient une vraie facture contre le TCC SFEC Terminal via le pont série : Python (pyserial), C# (System.IO.Ports + System.Text.Json) et Java (jSerialComm + org.json). Tous parlent au même port COM virtuel et envoient la même charge utile — le contrat ERPInvoice plat en camelCase. La logique métier est identique ; seule l’API série diffère.
votre client ──► COM10 ──► SfecTcpMiddleware (pont) ──► application terminal (Android SFEC App)SfecTcpMiddleware est l’outil Windows fourni que vous exécutez en Administrateur — vous ne l’implémentez ni ne le modifiez jamais. Démarrez le terminal d’abord, puis le pont, puis votre client. Le pont expose le port COM10 (115200 8N1 sans contrôle de flux), encadre chaque message avec END_JSON, et relaie au terminal. Le champ protocol de chaque réponse est interne — ignorez-le.
La facture d’exemple canonique
Un destinataire individual, une ligne product, TVA 18 %. Chaque valeur satisfait le validateur de cohérence.
{
"invoiceId": "INV-2026-0001",
"invoiceType": "salesInvoice",
"recipientType": "individual",
"isRecipientTaxable": false,
"subtotal": 10000.0,
"totalTaxTAmount": 1800.0,
"totalTaxRAmount": 0.0,
"totalExemptAmount": 0.0,
"totalTaxAmount": 1800.0,
"discountAmount": 0.0,
"totalLineDiscountAmount": 0.0,
"additionalCentTax": 0.0,
"electronicStampDuty": 0.0,
"totalAmount": 11800.0,
"amountDue": 11800.0,
"currency": "XAF",
"paymentMethod": "cash",
"items": [
{
"designation": "Produit test",
"type": "product",
"unitPrice": 10000.0,
"quantity": 1.0,
"subtotal": 10000.0,
"discountAmount": 0.0,
"discountType": "percentage",
"netAmount": 10000.0,
"taxRate": "18",
"taxAmount": 1800.0,
"totalAmount": 11800.0
}
]
}Preuve de cohérence : item subtotal = 10000×1 ; netAmount = 10000−0 ; item totalAmount = 10000+1800 = 11800 ; en-tête subtotal = 10000 ; taxes 1800+0 = 1800 ; totalAmount = 10000+1800−0+0+0 = 11800. Note : taxRate est une chaîne ("18").
Les trames sur le fil
→ {"invoiceId":"INV-2026-0001",...,"items":[...]}END_JSON
← {"status":"success","invoice_id":"INV-2026-0001","sfec_number":"SFEC-202607-00000001-4821",
"qr_code":"iVBORw0KGgoAAAANSUhEUgAA...","certification_date":1782864000000,
"message":"Invoice certified successfully","protocol":"TCP"}END_JSON
← {"status":"error","error_code":"VALIDATION_ERROR","invoice_id":"INV-2026-0001",
"errors":[{"field":"items[0].unitPrice","message":"Unit price must be greater than 0"}],
"timestamp":1782864000000,"protocol":"TCP"}END_JSON
← {"status":"error","invoice_id":"INV-2026-0001","error_code":"DUPLICATE_INVOICE",
"error_message":"Invoice already exists with this ID: INV-2026-0001","timestamp":1782864000000,"protocol":"TCP"}END_JSONClients de bout en bout
Python (pyserial)
Dépendance : pip install pyserial. Exécution : python sfec_serial_client.py.
#!/usr/bin/env python3
"""Client ERP de bout en bout pour le TCC SFEC Terminal via le pont série.
Encadrement : <json>END_JSON sur COM10, 115200 8N1, sans contrôle de flux. Requiert pyserial."""
import base64
import json
import time
from datetime import datetime, timezone
import serial # pyserial
COM_PORT = "COM10" # port exposé par le pont SfecTcpMiddleware
BAUD_RATE = 115200 # 8 bits de données, sans parité, 1 bit de stop, sans contrôle de flux
DELIMITER = b"END_JSON" # marqueur de trame ASCII littéral (jamais un saut de ligne)
INVOICE = {
"invoiceId": "INV-2026-0001",
"invoiceType": "salesInvoice",
"recipientType": "individual",
"isRecipientTaxable": False,
"subtotal": 10000.0,
"totalTaxTAmount": 1800.0,
"totalTaxRAmount": 0.0,
"totalExemptAmount": 0.0,
"totalTaxAmount": 1800.0,
"discountAmount": 0.0,
"totalLineDiscountAmount": 0.0,
"additionalCentTax": 0.0,
"electronicStampDuty": 0.0,
"totalAmount": 11800.0,
"amountDue": 11800.0,
"currency": "XAF",
"paymentMethod": "cash",
"items": [
{
"designation": "Produit test",
"type": "product",
"unitPrice": 10000.0,
"quantity": 1.0,
"subtotal": 10000.0,
"discountAmount": 0.0,
"discountType": "percentage",
"netAmount": 10000.0,
"taxRate": "18",
"taxAmount": 1800.0,
"totalAmount": 11800.0,
}
],
}
def send_frame(ser: serial.Serial, obj: dict) -> None:
ser.write(json.dumps(obj, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + DELIMITER)
ser.flush()
def read_frame(ser: serial.Serial, timeout: float = 35.0) -> dict:
"""Accumule les octets jusqu'à END_JSON, retire le marqueur et parse le JSON en amont."""
buffer = bytearray()
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
chunk = ser.read(ser.in_waiting or 1)
if chunk:
buffer.extend(chunk)
idx = buffer.find(DELIMITER)
if idx != -1:
return json.loads(buffer[:idx].decode("utf-8").strip())
raise TimeoutError(f"No frame within {timeout:.0f}s")
def health_check(ser: serial.Serial, timeout: float = 5.0) -> bool:
"""Sonde de liveness optionnelle : le pont répond au ping par un pong."""
send_frame(ser, {"type": "ping",
"timestamp": datetime.now(timezone.utc).isoformat(),
"source": "erp"})
try:
return read_frame(ser, timeout).get("type") == "pong"
except TimeoutError:
return False
def save_qr_png(qr_code_b64: str, path: str) -> int:
png = base64.b64decode(qr_code_b64) # Base64 brut, sans saut de ligne, sans préfixe data:
with open(path, "wb") as fh:
fh.write(png)
return len(png)
def main() -> None:
with serial.Serial(port=COM_PORT, baudrate=BAUD_RATE, bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE,
timeout=1, write_timeout=5) as ser:
if health_check(ser):
print("[HEALTH] bridge responded pong")
send_frame(ser, INVOICE)
resp = read_frame(ser)
if resp.get("status") == "success":
n = save_qr_png(resp["qr_code"], "qr_code.png")
print(f"[OK] sfec_number = {resp['sfec_number']}")
print(f"[OK] invoice_id = {resp['invoice_id']}")
print(f"[OK] certification_date = {resp['certification_date']} (epoch ms)")
print(f"[OK] qr_code.png = {n} bytes")
elif resp.get("error_code") == "VALIDATION_ERROR":
for e in resp.get("errors", []):
print(f"[VALIDATION] {e['field']}: {e['message']}")
else:
print(f"[ERROR] {resp.get('error_code')}: {resp.get('error_message')}")
if __name__ == "__main__":
main()Sortie de succès attendue
Sur succès, chaque client fait remonter les mêmes quatre faits — le sfec_number du terminal, le invoice_id renvoyé, la certification_date (epoch ms) et la taille du PNG QR décodé :
[HEALTH] bridge responded pong
[OK] sfec_number = SFEC-202607-00000001-XXXX
[OK] invoice_id = INV-2026-0001
[OK] certification_date = 1782864000000 (epoch ms)
[OK] qr_code.png = NNNN bytesPour porter ces clients vers un autre langage : ouvrez COM10 à 115200 8N1 sans contrôle de flux ; écrivez <json>END_JSON ; lisez jusqu’à voir END_JSON, retirez-le, et parsez le JSON en amont. C’est tout le protocole — pas de poignée de main, pas de heartbeat, pas de démultiplexage.
Étape suivante → FAQ