Initial commit: WebSocket ↔ Minitel GUI
This commit is contained in:
@@ -1,31 +1,172 @@
|
|||||||
# websocket2minitel
|
# WebSocket ↔ Minitel GUI
|
||||||
|
|
||||||
Very simple, quick and dirty python script creating a bridge between a websocket based vidotex server and a local Minitel connected through a serial port.
|
Interface graphique Python permettant de connecter un **Minitel (ou émulateur)** via un **port série** à un **serveur WebSocket** (MiniPAVI, BBS, etc.).
|
||||||
|
|
||||||
## Requirements
|
L’application assure une communication bidirectionnelle :
|
||||||
|
- WebSocket → Minitel
|
||||||
|
- Minitel → WebSocket
|
||||||
|
|
||||||
python modules:
|
Elle est compatible **Windows, Linux et macOS**.
|
||||||
- websockets
|
|
||||||
- pyserial
|
|
||||||
|
|
||||||
## Install
|
---
|
||||||
|
|
||||||
pip3 install -r requirements.txt
|
## Fonctionnalités
|
||||||
|
|
||||||
## Usage
|
- Interface graphique Tkinter simple
|
||||||
|
- Détection automatique des ports série
|
||||||
|
- Paramétrage complet :
|
||||||
|
- Vitesse (baudrate)
|
||||||
|
- Parité
|
||||||
|
- Bits de données
|
||||||
|
- Bits de stop
|
||||||
|
- Liste de serveurs WebSocket prédéfinis
|
||||||
|
- Support `ws://` et `wss://`
|
||||||
|
- Journalisation en temps réel
|
||||||
|
- Gestion asynchrone (WebSocket + Série)
|
||||||
|
|
||||||
python3 websocket2minitel.py <websocketURL> <serialPort> <serialSpeed>
|
---
|
||||||
|
|
||||||
## Examples
|
## Prérequis
|
||||||
|
|
||||||
Connect to 3615co.de and display at 1200 bps
|
- **Python 3.14** (ou ≥ 3.10 recommandé)
|
||||||
|
- Un Minitel réel ou un émulateur série
|
||||||
|
- Accès à un serveur WebSocket Minitel
|
||||||
|
|
||||||
`python3 websocket2minitel.py 'wss://3615co.de/ws' /dev/ttyUSB0 1200`
|
---
|
||||||
|
|
||||||
Connect to 3611.re (Annuaire Electronique re-creation)
|
## Installation
|
||||||
|
|
||||||
`python3 websocket2minitel.py 'ws://3611.re/ws' /dev/ttyUSB0 1200`
|
### 1. Cloner le projet
|
||||||
|
|
||||||
Connect to 3614 HACKER revival and display at 4800 bps (Minitel 1 bistandard and above)
|
```bash
|
||||||
|
git clone https://github.com/labbej27websocket-minitel.git
|
||||||
|
cd websocket-minitel
|
||||||
|
```
|
||||||
|
- Créer un environnement virtuel (recommandé)
|
||||||
|
```bash
|
||||||
|
python3.14 -m venv venv
|
||||||
|
```
|
||||||
|
### 2. Activation :
|
||||||
|
|
||||||
`python3 websocket2minitel.py 'ws://mntl.joher.com:2018' /dev/ttyUSB0 4800`
|
Windows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux / macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
### 4. Installer les dépendances
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
### 5. Lancement du programme
|
||||||
|
```bash
|
||||||
|
python websocket_minitel.py
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Une fenêtre graphique s’ouvre permettant de configurer et lancer la connexion.
|
||||||
|
|
||||||
|
## Compilation en exécutable
|
||||||
|
La compilation permet d’obtenir un binaire autonome (sans Python requis).
|
||||||
|
|
||||||
|
Outil utilisé
|
||||||
|
PyInstaller
|
||||||
|
|
||||||
|
### Installation :
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyinstaller
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compilation Windows
|
||||||
|
```bash
|
||||||
|
pyinstaller ^
|
||||||
|
--onefile ^
|
||||||
|
--windowed ^
|
||||||
|
--name websocket-minitel ^
|
||||||
|
websocket_minitel.py
|
||||||
|
```
|
||||||
|
### Résultat :
|
||||||
|
|
||||||
|
- dist/websocket-minitel.exe
|
||||||
|
### Compilation Linux
|
||||||
|
```bash
|
||||||
|
pyinstaller \
|
||||||
|
--onefile \
|
||||||
|
--windowed \
|
||||||
|
--name websocket-minitel \
|
||||||
|
websocket_minitel.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Résultat :
|
||||||
|
|
||||||
|
- dist/websocket-minitel
|
||||||
|
-
|
||||||
|
## L’exécutable est spécifique à l’OS :
|
||||||
|
|
||||||
|
- Un .exe Windows doit être compilé sous Windows, idem pour Linux/macOS.
|
||||||
|
|
||||||
|
## Compilation macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyinstaller \
|
||||||
|
--onefile \
|
||||||
|
--windowed \
|
||||||
|
--name websocket-minitel \
|
||||||
|
websocket_minitel.py
|
||||||
|
```
|
||||||
|
### Résultat :
|
||||||
|
|
||||||
|
- dist/websocket-minitel.app
|
||||||
|
#### Note macOS (SSL) :
|
||||||
|
- Le script utilise un contexte SSL non vérifié pour éviter certains problèmes de certificats (wss://) sur macOS.
|
||||||
|
|
||||||
|
#### Ports série – Permissions
|
||||||
|
- Linux
|
||||||
|
Ajouter l’utilisateur au groupe dialout :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -a -G dialout $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis redémarrer la session.
|
||||||
|
|
||||||
|
#### macOS
|
||||||
|
Autoriser l’accès au port série dans :
|
||||||
|
|
||||||
|
-Réglages → Sécurité et confidentialité → Confidentialité → Accès complet au disque
|
||||||
|
|
||||||
|
### Serveurs WebSocket intégrés
|
||||||
|
- MiniPAVI (officiel)
|
||||||
|
|
||||||
|
- Hacker
|
||||||
|
|
||||||
|
- Annuaire
|
||||||
|
|
||||||
|
- 3615
|
||||||
|
|
||||||
|
- Retrocampus
|
||||||
|
|
||||||
|
- LABBEJ27
|
||||||
|
|
||||||
|
- Saisie manuelle
|
||||||
|
|
||||||
|
### Dépannage
|
||||||
|
- Vérifier le port série sélectionné
|
||||||
|
|
||||||
|
- Vérifier la vitesse (souvent 1200 bauds pour Minitel)
|
||||||
|
|
||||||
|
- Tester sans SSL (ws://) si possible
|
||||||
|
|
||||||
|
- Lancer depuis un terminal pour voir les erreurs
|
||||||
|
|
||||||
|
#### Licence
|
||||||
|
Projet libre – utilisation et modification autorisées.
|
||||||
|
---
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, scrolledtext
|
||||||
|
import serial.tools.list_ports
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import serial
|
||||||
|
import websockets
|
||||||
|
import time
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
# SSL macOS workaround
|
||||||
|
ssl_context = ssl._create_unverified_context()
|
||||||
|
|
||||||
|
ser = None
|
||||||
|
ws = None
|
||||||
|
running = False
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# LOG
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
def log(text, log_widget):
|
||||||
|
timestamp = time.strftime("%H:%M:%S")
|
||||||
|
log_widget.configure(state="normal")
|
||||||
|
log_widget.insert(tk.END, f"[{timestamp}] {text}\n")
|
||||||
|
log_widget.see(tk.END)
|
||||||
|
log_widget.configure(state="disabled")
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# ASYNC TASK : WebSocket ↔ Minitel
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
async def websocket_task(url, tty, speed, parity, databits, stopbits, status_label, log_widget):
|
||||||
|
global ser, ws, running
|
||||||
|
|
||||||
|
STOPBITS_MAP = {
|
||||||
|
"1": serial.STOPBITS_ONE,
|
||||||
|
"1.5": serial.STOPBITS_ONE_POINT_FIVE,
|
||||||
|
"2": serial.STOPBITS_TWO
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ouverture port série
|
||||||
|
log(f"Ouverture du port série {tty} à {speed} bauds…", log_widget)
|
||||||
|
ser = serial.Serial(
|
||||||
|
tty,
|
||||||
|
int(speed),
|
||||||
|
parity=parity,
|
||||||
|
bytesize=int(databits),
|
||||||
|
stopbits=STOPBITS_MAP[stopbits],
|
||||||
|
timeout=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Connexion WebSocket
|
||||||
|
log(f"Connexion au WebSocket {url}…", log_widget)
|
||||||
|
|
||||||
|
if url.startswith("wss://"):
|
||||||
|
log("Utilisation d’un contexte SSL non vérifié (wss)…", log_widget)
|
||||||
|
ws = await websockets.connect(url, ssl=ssl_context)
|
||||||
|
else:
|
||||||
|
ws = await websockets.connect(url)
|
||||||
|
|
||||||
|
log("Connexion établie.", log_widget)
|
||||||
|
status_label.config(text="Connecté")
|
||||||
|
|
||||||
|
# Message test Minitel
|
||||||
|
ser.write(b"\x07\x0c\x1f\x40\x41connexion\x0a")
|
||||||
|
ser.write(b"\x1b\x3b\x60\x58\x52")
|
||||||
|
|
||||||
|
# WebSocket → Minitel
|
||||||
|
async def w2m():
|
||||||
|
while running:
|
||||||
|
try:
|
||||||
|
data = await ws.recv()
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
ser.write(data)
|
||||||
|
log(f"[WS→Minitel] {len(data)} octets", log_widget)
|
||||||
|
else:
|
||||||
|
ser.write(data.encode("latin1", "replace"))
|
||||||
|
log(f"[WS→Minitel] {data}", log_widget)
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Minitel → WebSocket
|
||||||
|
async def m2w():
|
||||||
|
while running:
|
||||||
|
if ser.in_waiting > 0:
|
||||||
|
data = ser.read(ser.in_waiting)
|
||||||
|
await ws.send(data.decode("latin1", "replace"))
|
||||||
|
log(f"[Minitel→WS] {data}", log_widget)
|
||||||
|
else:
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
|
||||||
|
await asyncio.gather(w2m(), m2w())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Erreur : {e}", log_widget)
|
||||||
|
status_label.config(text="Erreur")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
running = False
|
||||||
|
log("Déconnexion…", log_widget)
|
||||||
|
try:
|
||||||
|
if ws:
|
||||||
|
await ws.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if ser:
|
||||||
|
ser.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
status_label.config(text="Déconnecté")
|
||||||
|
log("Connexions fermées.", log_widget)
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# THREAD RUNNER
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
def start_async(url, tty, speed, parity, databits, stopbits, status_label, log_widget):
|
||||||
|
global running
|
||||||
|
if running:
|
||||||
|
return
|
||||||
|
running = True
|
||||||
|
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
threading.Thread(
|
||||||
|
target=loop.run_until_complete,
|
||||||
|
args=(websocket_task(url, tty, speed, parity, databits, stopbits, status_label, log_widget),),
|
||||||
|
daemon=True
|
||||||
|
).start()
|
||||||
|
|
||||||
|
|
||||||
|
def stop_connection(status_label, log_widget):
|
||||||
|
global running
|
||||||
|
running = False
|
||||||
|
status_label.config(text="Déconnecté")
|
||||||
|
log("Arrêt demandé.", log_widget)
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Port detection refresh
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
def update_ports(combo):
|
||||||
|
current_ports = set(combo["values"])
|
||||||
|
detected = {p.device for p in serial.tools.list_ports.comports()}
|
||||||
|
|
||||||
|
if detected != current_ports:
|
||||||
|
combo["values"] = list(detected)
|
||||||
|
if detected:
|
||||||
|
combo.set(list(detected)[0])
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# GUI
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
def build_gui():
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("WebSocket ↔ Minitel")
|
||||||
|
|
||||||
|
# LISTE DE SERVEURS
|
||||||
|
SERVERS = {
|
||||||
|
"MiniPAVI (officiel)": "wss://go.minipavi.fr:8181",
|
||||||
|
"Hacker": "ws://mntl.joher.com:2018",
|
||||||
|
"Annuaire": "ws://3611.re/ws",
|
||||||
|
"3615": "ws://3615co.de/ws",
|
||||||
|
"Retrocampus": "wss://bbs.retrocampus.com:8051",
|
||||||
|
"LABBEJ27": "wss://minitel.labbej.fr:8182",
|
||||||
|
"Saisie manuelle…": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
tk.Label(root, text="Serveur prédéfini").grid(row=0, column=0)
|
||||||
|
server_combo = ttk.Combobox(root, values=list(SERVERS.keys()), width=40)
|
||||||
|
server_combo.set("MiniPAVI (officiel)")
|
||||||
|
server_combo.grid(row=0, column=1)
|
||||||
|
|
||||||
|
# Champ URL modifiable
|
||||||
|
tk.Label(root, text="Adresse WebSocket").grid(row=1, column=0)
|
||||||
|
url_entry = tk.Entry(root, width=40)
|
||||||
|
url_entry.insert(0, SERVERS["MiniPAVI (officiel)"])
|
||||||
|
url_entry.grid(row=1, column=1)
|
||||||
|
|
||||||
|
def on_server_change(event):
|
||||||
|
url = SERVERS.get(server_combo.get(), "")
|
||||||
|
url_entry.delete(0, tk.END)
|
||||||
|
url_entry.insert(0, url)
|
||||||
|
|
||||||
|
server_combo.bind("<<ComboboxSelected>>", on_server_change)
|
||||||
|
|
||||||
|
# PORT SERIE
|
||||||
|
tk.Label(root, text="Port série").grid(row=2, column=0)
|
||||||
|
ports = [p.device for p in serial.tools.list_ports.comports()]
|
||||||
|
port_combo = ttk.Combobox(root, values=ports, width=20)
|
||||||
|
if ports:
|
||||||
|
port_combo.set(ports[0])
|
||||||
|
port_combo.grid(row=2, column=1)
|
||||||
|
|
||||||
|
# VITESSE
|
||||||
|
tk.Label(root, text="Vitesse").grid(row=3, column=0)
|
||||||
|
speeds = ["1200", "4800", "9600", "19200"]
|
||||||
|
speed_combo = ttk.Combobox(root, values=speeds, width=20)
|
||||||
|
speed_combo.set("1200")
|
||||||
|
speed_combo.grid(row=3, column=1)
|
||||||
|
|
||||||
|
# PARITÉ
|
||||||
|
tk.Label(root, text="Parité").grid(row=4, column=0)
|
||||||
|
parity_map = {
|
||||||
|
"Even (pair)": serial.PARITY_EVEN,
|
||||||
|
"Odd (impair)": serial.PARITY_ODD,
|
||||||
|
"None": serial.PARITY_NONE,
|
||||||
|
"Mark": serial.PARITY_MARK,
|
||||||
|
"Space": serial.PARITY_SPACE
|
||||||
|
}
|
||||||
|
parity_combo = ttk.Combobox(root, values=list(parity_map.keys()), width=20)
|
||||||
|
parity_combo.set("Even (pair)")
|
||||||
|
parity_combo.grid(row=4, column=1)
|
||||||
|
|
||||||
|
# DATABITS
|
||||||
|
tk.Label(root, text="Bits de données").grid(row=5, column=0)
|
||||||
|
databits_combo = ttk.Combobox(root, values=["7", "8"], width=20)
|
||||||
|
databits_combo.set("7")
|
||||||
|
databits_combo.grid(row=5, column=1)
|
||||||
|
|
||||||
|
# STOPBITS
|
||||||
|
tk.Label(root, text="Bits de stop").grid(row=6, column=0)
|
||||||
|
stopbits_combo = ttk.Combobox(root, values=["1", "1.5", "2"], width=20)
|
||||||
|
stopbits_combo.set("1")
|
||||||
|
stopbits_combo.grid(row=6, column=1)
|
||||||
|
|
||||||
|
# LOG
|
||||||
|
log_widget = scrolledtext.ScrolledText(root, width=60, height=15, state="disabled")
|
||||||
|
log_widget.grid(row=8, column=0, columnspan=2, padx=5, pady=5)
|
||||||
|
|
||||||
|
# STATUT
|
||||||
|
status_label = tk.Label(root, text="En attente…")
|
||||||
|
status_label.grid(row=9, column=0, columnspan=2)
|
||||||
|
|
||||||
|
# BOUTONS
|
||||||
|
tk.Button(
|
||||||
|
root,
|
||||||
|
text="Connecter",
|
||||||
|
command=lambda: start_async(
|
||||||
|
url_entry.get(),
|
||||||
|
port_combo.get(),
|
||||||
|
speed_combo.get(),
|
||||||
|
parity_map[parity_combo.get()],
|
||||||
|
databits_combo.get(),
|
||||||
|
stopbits_combo.get(),
|
||||||
|
status_label,
|
||||||
|
log_widget
|
||||||
|
)
|
||||||
|
).grid(row=7, column=0)
|
||||||
|
|
||||||
|
tk.Button(
|
||||||
|
root,
|
||||||
|
text="Déconnecter",
|
||||||
|
command=lambda: stop_connection(status_label, log_widget)
|
||||||
|
).grid(row=7, column=1)
|
||||||
|
|
||||||
|
# REFRESH PORTS
|
||||||
|
def refresh_ports():
|
||||||
|
update_ports(port_combo)
|
||||||
|
root.after(1000, refresh_ports)
|
||||||
|
|
||||||
|
refresh_ports()
|
||||||
|
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
build_gui()
|
||||||
Reference in New Issue
Block a user