miniminitel 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: miniminitel
3
+ Version: 1.0.3
4
+ Summary: Simple Minitel interface in Python
5
+ Author: Portevent
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Portevent/MiniMinitel
8
+ Project-URL: Issues, https://github.com/Portevent/MiniMinitel/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: pillow
13
+ Requires-Dist: pyserial
14
+
15
+ # MiniMinitel
16
+ MiniMinitel is a simple Minitel interface in python. It has been created with the help of :
17
+ - http://543210.free.fr/TV/stum1b.pdf
18
+ - https://wiki-ima.plil.fr/mediawiki/images/b/b1/Minitel.pdf
19
+ - https://github.com/eserandour/Minitel1B_Hard
20
+
21
+ ```python
22
+ from miniminitel import MiniMinitel
23
+
24
+ with MiniMinitel("COM4") as minitel:
25
+ minitel.clear()
26
+
27
+ minitel.cursor_move_to(4, 4)
28
+ minitel.write_double_grandeur("Hello world")
29
+
30
+ minitel.write_at(6, 6, "I'm a MiniMinitel")
31
+ minitel.set_blinking()
32
+ minitel.write_at(6, 7, "And I look cool")
33
+ minitel.set_non_blinking()
34
+
35
+ ```
36
+
37
+ # Setup
38
+ ## Python
39
+ MiniMinitel is a work in progress, no version are published to PyPI yet.
40
+ Library is written with Python 3.10, no support for other version are guaranteed (should be compatible Python 3.11+)
41
+ ```commandline
42
+ pip install miniminitel
43
+ ```
44
+
45
+ ## Minitel
46
+ Minitel need to be pluggged to computer throught serial port. Documentation need to be written
@@ -0,0 +1,32 @@
1
+ # MiniMinitel
2
+ MiniMinitel is a simple Minitel interface in python. It has been created with the help of :
3
+ - http://543210.free.fr/TV/stum1b.pdf
4
+ - https://wiki-ima.plil.fr/mediawiki/images/b/b1/Minitel.pdf
5
+ - https://github.com/eserandour/Minitel1B_Hard
6
+
7
+ ```python
8
+ from miniminitel import MiniMinitel
9
+
10
+ with MiniMinitel("COM4") as minitel:
11
+ minitel.clear()
12
+
13
+ minitel.cursor_move_to(4, 4)
14
+ minitel.write_double_grandeur("Hello world")
15
+
16
+ minitel.write_at(6, 6, "I'm a MiniMinitel")
17
+ minitel.set_blinking()
18
+ minitel.write_at(6, 7, "And I look cool")
19
+ minitel.set_non_blinking()
20
+
21
+ ```
22
+
23
+ # Setup
24
+ ## Python
25
+ MiniMinitel is a work in progress, no version are published to PyPI yet.
26
+ Library is written with Python 3.10, no support for other version are guaranteed (should be compatible Python 3.11+)
27
+ ```commandline
28
+ pip install miniminitel
29
+ ```
30
+
31
+ ## Minitel
32
+ Minitel need to be pluggged to computer throught serial port. Documentation need to be written
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "miniminitel"
3
+ version = "1.0.3"
4
+ authors = [
5
+ { name="Portevent" },
6
+ ]
7
+ description = "Simple Minitel interface in Python"
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3"
12
+ ]
13
+ license = "MIT"
14
+ license-files = ["LICEN[CS]E*"]
15
+ dependencies = [
16
+ "pillow",
17
+ "pyserial"
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/Portevent/MiniMinitel"
22
+ Issues = "https://github.com/Portevent/MiniMinitel/issues"
23
+
24
+ [build-system]
25
+ requires = ["setuptools >= 80.9.0"]
26
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,39 @@
1
+ # C0 grid
2
+
3
+ # http://543210.free.fr/TV/stum1b.pdf - page 92 (95 in pdf)
4
+
5
+ NUL : bytes = b'\x00' # Documentation required
6
+ SCH : bytes = b'\x01' # Documentation required
7
+
8
+
9
+ EOT : bytes = b'\x04' # Documentation required
10
+ ENQ : bytes = b'\x05' # Documentation required
11
+
12
+ BEL : bytes = b'\x07' # Ring a bell
13
+ BS : bytes = b'\x08' # Ctrl + H Move cursor left
14
+ HT : bytes = b'\x09' # Ctrl + I Move cursor right
15
+ TAB : bytes = HT # HT is sometimes called TAB
16
+ LF : bytes = b'\x0A' # Ctrl + J Move cursor down
17
+ VT : bytes = b'\x0B' # Ctrl + K Move cursor up
18
+ FF : bytes = b'\x0C' # Return cursor to start of first line. Clear all. Explicit article separator
19
+ CR : bytes = b'\x0D' # <--' Move cursor to origin
20
+ SO : bytes = b'\x0E' # Switch to G1 characters
21
+ SI : bytes = b'\x0F' # Revert to standart alphanum characters (G0)
22
+
23
+ DLE : bytes = b'\x10' # Documentation required
24
+ Con : bytes = b'\x11' # Cursor on
25
+ Rep : bytes = b'\x12' # Documentation required
26
+ Sep : bytes = b'\x13' # Documentation required
27
+ Coff : bytes = b'\x14' # Cursor off
28
+ NACK : bytes = b'\x15' # Documentation required
29
+ SYN : bytes = b'\x16' # Documentation required
30
+
31
+ CAN : bytes = b'\x18' # Ctrl + X Add space till end of line
32
+ SS2 : bytes = b'\x19' # Documentation required
33
+ SUB : bytes = b'\x1A' # Documentation required
34
+ ESC : bytes = b'\x1B' # Esc code (used to access C1 grid)
35
+ SS3 : bytes = b'\x1D' # Documentation required
36
+ RS : bytes = b'\x1E' # Return cursor to start of first line. Explicit article separator
37
+
38
+ US : bytes = b'\x1F' # Explicit article separator
39
+
@@ -0,0 +1,37 @@
1
+ # C1 grid
2
+
3
+ # http://543210.free.fr/TV/stum1b.pdf - page 92 (95 in pdf)
4
+
5
+ NOIR : bytes = b'\x40'
6
+ ROUGE : bytes = b'\x41'
7
+ VERT : bytes = b'\x42'
8
+ JAUNE : bytes = b'\x43'
9
+ BLEU : bytes = b'\x44'
10
+ MAJENTA : bytes = b'\x45'
11
+ CYAN : bytes = b'\x46'
12
+ BLANC : bytes = b'\x47'
13
+ CLIGNOTEMENT : bytes = b'\x48'
14
+ FIXE : bytes = b'\x49'
15
+
16
+
17
+ GRANDEUR_NORMALE : bytes = b'\x4C'
18
+ DOUBLE_HAUTEUR : bytes = b'\x4D'
19
+ DOUBLE_LARGEUR : bytes = b'\x4E'
20
+ DOUBLE_GRANDEUR : bytes = b'\x4F'
21
+
22
+ FOND_NOIR : bytes = b'\x50'
23
+ FOND_ROUGE : bytes = b'\x51'
24
+ FOND_VERT : bytes = b'\x52'
25
+ FOND_JAUNE : bytes = b'\x53'
26
+ FOND_BLEU : bytes = b'\x54'
27
+ FOND_MAJENTA : bytes = b'\x55'
28
+ FOND_CYAN : bytes = b'\x56'
29
+ FOND_BLANC : bytes = b'\x57'
30
+
31
+ MASQUAGE : bytes = b'\x58'
32
+ LIGNAGE_FIN : bytes = b'\x59'
33
+ LIGNAGE_DEBUT : bytes = b'\x5A'
34
+ FOND_NORMAL : bytes = b'\x5C'
35
+ FOND_INVERSE : bytes = b'\x5D'
36
+ DEMASQUAGE : bytes = b'\x5F'
37
+
@@ -0,0 +1 @@
1
+ from .miniminitel import MiniMinitel
@@ -0,0 +1,5 @@
1
+ from .minitel_image import MinitelImage
2
+
3
+
4
+ class MiniMinitel(MinitelImage):
5
+ pass
@@ -0,0 +1,183 @@
1
+ from typing import Callable
2
+
3
+ from .C0 import ESC, BS, LF, CAN, TAB, Sep
4
+ from .minitel_dialog_code import MinitelInput, MinitelCode
5
+ from .minitel_videotex_attributes import MinitelVideotexAttributes
6
+
7
+
8
+ class MinitelController(MinitelVideotexAttributes):
9
+
10
+ def write(self, text: str):
11
+ """
12
+ Write text on Minitel
13
+ :param text: text
14
+ """
15
+ self._write_byte(bytes(text, encoding="ascii"))
16
+
17
+ def write_at(self, x: int, y: int, text: str) -> None:
18
+ """
19
+ Move cursor to position and write to
20
+ :param x: Column
21
+ :param y: Row
22
+ :param text: Text
23
+ """
24
+ self.cursor_move_to(x, y)
25
+ self.write(text)
26
+
27
+ def write_double_grandeur(self, text: str) -> None:
28
+ """
29
+ Write with double grandeur
30
+ :param text: Text
31
+ """
32
+ self.set_double_grandeur()
33
+ self.write(text)
34
+ self.set_grandeur_normale()
35
+
36
+ def write_double_largeur(self, text: str):
37
+ """
38
+ Write with double largeur
39
+ :param text: Text
40
+ """
41
+ self.set_double_largeur()
42
+ self.write(text)
43
+ self.set_grandeur_normale()
44
+
45
+ def write_double_hauteur(self, text: str):
46
+ """
47
+ Write with double hauteur
48
+ :param text: Text
49
+ """
50
+ self.set_double_hauteur()
51
+ self.write(text)
52
+ self.set_grandeur_normale()
53
+
54
+ def start_listening(self, on_read: Callable[[MinitelInput], None]):
55
+ """
56
+ Listen to port and pass MinitelInput to callback
57
+ :param on_read: Action to perform on read
58
+ """
59
+ value = self.ser.read_all() # Clear buffer before, usefull to get rid of bootup code
60
+ # print(f"READALL -> {value}")
61
+
62
+ while True:
63
+ value = self.ser.read()
64
+ print(f" -> {value}")
65
+
66
+ if value == ESC: # Mode téléinformatique
67
+ word = self.ser.read() + self.ser.read()
68
+
69
+ match word:
70
+ case b'OM':
71
+ on_read(MinitelInput(MinitelCode.ENVOI))
72
+
73
+ case b'OP':
74
+ on_read(MinitelInput(MinitelCode.SOMMAIRE))
75
+
76
+ case b'OQ':
77
+ on_read(MinitelInput(MinitelCode.ANNULATION))
78
+
79
+ case b'OR':
80
+ on_read(MinitelInput(MinitelCode.RETOUR))
81
+
82
+ case b'OS':
83
+ on_read(MinitelInput(MinitelCode.REPETITION))
84
+
85
+ case b'Om':
86
+ on_read(MinitelInput(MinitelCode.GUIDE))
87
+
88
+ case b'Ol':
89
+ on_read(MinitelInput(MinitelCode.CORRECTION))
90
+
91
+ case b'On':
92
+ on_read(MinitelInput(MinitelCode.SUITE))
93
+
94
+ case b'[A':
95
+ on_read(MinitelInput(MinitelCode.MOVE_UP))
96
+
97
+ case b'[B':
98
+ on_read(MinitelInput(MinitelCode.MOVE_DOWN))
99
+
100
+ case b'[C':
101
+ on_read(MinitelInput(MinitelCode.MOVE_RIGHT))
102
+
103
+ case b'[D':
104
+ on_read(MinitelInput(MinitelCode.MOVE_LEFT))
105
+
106
+ case b'[H':
107
+ on_read(MinitelInput(MinitelCode.GO_BACK_UP))
108
+
109
+ case b'[L':
110
+ on_read(MinitelInput(MinitelCode.INSERT_LINE))
111
+
112
+ case b'[M':
113
+ on_read(MinitelInput(MinitelCode.SUPPR_LINE))
114
+
115
+ case b'[P':
116
+ on_read(MinitelInput(MinitelCode.SUPPR_COLUMN))
117
+
118
+ case b'[4':
119
+ word += self.ser.read()
120
+ # print(f"INSERT COLUMN {word}")
121
+ on_read(MinitelInput(MinitelCode.INSERT_COLUMN, word.decode()))
122
+
123
+ case b'[2':
124
+ word += self.ser.read()
125
+ # print(f"E.Page {word}")
126
+ on_read(MinitelInput(MinitelCode.E_Page, word.decode()))
127
+
128
+ case _:
129
+ on_read(MinitelInput(MinitelCode.UNKNOWN_CODE, word.decode()))
130
+
131
+ elif value == Sep: # Mode télétel
132
+ word = self.ser.read()
133
+
134
+ match word:
135
+ case b'A':
136
+ on_read(MinitelInput(MinitelCode.ENVOI))
137
+
138
+ case b'F':
139
+ on_read(MinitelInput(MinitelCode.SOMMAIRE))
140
+
141
+ case b'E':
142
+ on_read(MinitelInput(MinitelCode.ANNULATION))
143
+
144
+ case b'B':
145
+ on_read(MinitelInput(MinitelCode.RETOUR))
146
+
147
+ case b'C':
148
+ on_read(MinitelInput(MinitelCode.REPETITION))
149
+
150
+ case b'D':
151
+ on_read(MinitelInput(MinitelCode.GUIDE))
152
+
153
+ case b'G':
154
+ on_read(MinitelInput(MinitelCode.CORRECTION))
155
+
156
+ case b'H':
157
+ on_read(MinitelInput(MinitelCode.SUITE))
158
+
159
+ case b'Y':
160
+ on_read(MinitelInput(MinitelCode.CONNEXION))
161
+
162
+ case _:
163
+ on_read(MinitelInput(MinitelCode.UNKNOWN_CODE, word.decode()))
164
+ else:
165
+ # Can't add this check in match, as match value: case BS: will map value onto BS
166
+ if value == BS:
167
+ on_read(MinitelInput(MinitelCode.BS)) # Ctrl + H
168
+ elif value == LF:
169
+ on_read(MinitelInput(MinitelCode.LF)) # Ctrl + J
170
+ elif value == CAN:
171
+ on_read(MinitelInput(MinitelCode.CAN)) # Ctrl + X
172
+ elif value == TAB:
173
+ on_read(MinitelInput(MinitelCode.TAB)) # Ctrl + I
174
+ else:
175
+ match value:
176
+ case b'\x00':
177
+ on_read(MinitelInput(MinitelCode.BRK)) # Ctrl + Connexion
178
+ case b'\x7f':
179
+ on_read(MinitelInput(MinitelCode.DELETE)) # Ctrl + Connexion
180
+ case b'\r':
181
+ on_read(MinitelInput(MinitelCode.CARRIAGE_RETURN)) # ENTER
182
+ case _:
183
+ on_read(MinitelInput(MinitelCode.TEXT, value.decode()))
@@ -0,0 +1,142 @@
1
+ from .minitel_videotex import MinitelVideotex
2
+
3
+ class MinitelCSI(MinitelVideotex):
4
+ """
5
+ Minitel CSI commands
6
+ """
7
+
8
+ def move_cursor_up(self, n: int = 1):
9
+ """
10
+ Move cursor up n time
11
+ Note : when n = 1, this function is similar to shiftCursorUp()
12
+ """
13
+ self._write_csi()
14
+ self._write_bytes_p(n)
15
+ self._write_byte(b'\x41')
16
+
17
+ def move_cursor_down(self, n: int = 1):
18
+ """
19
+ Move cursor down n time
20
+ Note : when n = 1, this function is similar to shiftCursorDown()
21
+ """
22
+ self._write_csi()
23
+ self._write_bytes_p(n)
24
+ self._write_byte(b'\x42')
25
+
26
+ def move_cursor_right(self, n: int = 1):
27
+ """
28
+ Move cursor right n time
29
+ Note : when n = 1, this function is similar to shiftCursorRight()
30
+ """
31
+ self._write_csi()
32
+ self._write_bytes_p(n)
33
+ self._write_byte(b'\x43')
34
+
35
+ def move_cursor_left(self, n: int = 1):
36
+ """
37
+ Move cursor left n time
38
+ Note : when n = 1, this function is similar to shiftCursorLeft()
39
+ """
40
+ self._write_csi()
41
+ self._write_bytes_p(n)
42
+ self._write_byte(b'\x44')
43
+
44
+ def cursor_move_to(self, x: int, y: int):
45
+ """
46
+ Move cursor to row X and column Y
47
+ :param x: from 01 to 24
48
+ :param y: from 01 to 40 (or 80)
49
+ """
50
+ self._write_csi()
51
+ self._write_bytes_p(y)
52
+ self._write_byte(b'\x3B')
53
+ self._write_bytes_p(x)
54
+ self._write_byte(b'\x48')
55
+
56
+ def clear_all_after(self):
57
+ """
58
+ Clear everything after the cursor till last row (doesn't move cursor)
59
+ """
60
+ self._write_csi()
61
+ # self._writeByte(b'\x30') Optional, doesn't matter
62
+ self._write_byte(b'\x4A')
63
+
64
+ def clear_all_before(self):
65
+ """
66
+ Clear everything before the cursor till first row (doesn't move cursor)
67
+ """
68
+ self._write_csi()
69
+ self._write_byte(b'\x31')
70
+ self._write_byte(b'\x4A')
71
+
72
+ def clear_screen(self):
73
+ """
74
+ Clear everything (doesn't move cursor)
75
+ """
76
+ self._write_csi()
77
+ self._write_byte(b'\x32')
78
+ self._write_byte(b'\x4A')
79
+
80
+ def clear_row_end(self):
81
+ """
82
+ Clear after the cursor till the end of row (doesn't move cursor)
83
+ """
84
+ self._write_csi()
85
+ # self._writeByte(b'\x30') Optional
86
+ self._write_byte(b'\x4B')
87
+
88
+ def clear_row_start(self):
89
+ """
90
+ Clear before the cursor till the start of row (doesn't move cursor)
91
+ """
92
+ self._write_csi()
93
+ self._write_byte(b'\x31')
94
+ self._write_byte(b'\x4B')
95
+
96
+ def clear_row(self):
97
+ """
98
+ Clear current row (doesn't move cursor)
99
+ """
100
+ self._write_csi()
101
+ self._write_byte(b'\x32')
102
+ self._write_byte(b'\x4B')
103
+
104
+ def insert_rows(self, count: int):
105
+ """
106
+ Insert n rows
107
+ """
108
+ self._write_csi()
109
+ self._write_bytes_p(count)
110
+ self._write_byte(b'\x4C')
111
+
112
+ def clear_rows(self, count: int):
113
+ """
114
+ Clear n rows
115
+ """
116
+ self._write_csi()
117
+ self._write_bytes_p(count)
118
+ self._write_byte(b'\x4D')
119
+
120
+ def clear_characters(self, count: int):
121
+ """
122
+ Clear n character
123
+ """
124
+ self._write_csi()
125
+ self._write_bytes_p(count)
126
+ self._write_byte(b'\x50')
127
+
128
+ def insert_mode_on(self):
129
+ """
130
+ Activate insertion mode
131
+ """
132
+ self._write_csi()
133
+ self._write_byte(b'\x34')
134
+ self._write_byte(b'\x68')
135
+
136
+ def insert_mode_off(self):
137
+ """
138
+ Deactivate insertion mode
139
+ """
140
+ self._write_csi()
141
+ self._write_byte(b'\x34')
142
+ self._write_byte(b'\x6C')
@@ -0,0 +1,38 @@
1
+ class MinitelCode:
2
+ TEXT = 'TEXT'
3
+ ENVOI = 'ENVOI'
4
+ SOMMAIRE = "SOMMAIRE"
5
+ ANNULATION = "ANNULATION"
6
+ RETOUR = "RETOUR"
7
+ REPETITION = "REPETITION"
8
+ GUIDE = 'GUIDE'
9
+ CORRECTION = "CORRECTION"
10
+ SUITE = "SUITE"
11
+ MOVE_UP = "MOVE UP"
12
+ MOVE_DOWN = "MOVE DOWN"
13
+ MOVE_RIGHT = "MOVE RIGHT"
14
+ MOVE_LEFT = "MOVE LEFT"
15
+ GO_BACK_UP = "GO BACK UP"
16
+ INSERT_LINE = "INSERT LINE"
17
+ SUPPR_LINE = "SUPPR LINE"
18
+ SUPPR_COLUMN = "SUPPR COLUMN"
19
+ INSERT_COLUMN = "INSERT COLUMN"
20
+ E_Page = "E.Page"
21
+ CONNEXION = "CONNEXION"
22
+ BS = "BS"
23
+ LF = "LF"
24
+ CAN = "CAN"
25
+ TAB = "TAB"
26
+ BRK = "BRK"
27
+ DELETE = "DELETE"
28
+ CARRIAGE_RETURN = "CARRIAGE RETURN"
29
+ UNKNOWN_CODE = "UNKNOWN CODE"
30
+
31
+ class MinitelInput:
32
+ code: str
33
+ text: str
34
+
35
+ def __init__(self, code: str, text: str = ""):
36
+ self.code = code
37
+ self.text = text
38
+
@@ -0,0 +1,234 @@
1
+ from random import random
2
+
3
+ from PIL import Image
4
+
5
+ from .minitel_controller import MinitelController
6
+
7
+ def pseudo_random(min_value: int, max_value: int, bias = 1):
8
+ return min_value + int((max_value - min_value) * (random() ** bias))
9
+
10
+ # Palette Minitel (8 couleurs) en RGB
11
+ minitel_palette = [
12
+ (0, 0, 0), # Noir
13
+ (255, 0, 0), # Rouge
14
+ (0, 255, 0), # Vert
15
+ (255, 255, 0), # Jaune
16
+ (0, 0, 255), # Bleu
17
+ (255, 0, 255), # Magenta
18
+ (0, 255, 255), # Cyan
19
+ (255, 255, 255) # Blanc
20
+ ]
21
+
22
+ pixel_to_g1_code = {
23
+ "000000": "20", "100000": "21", "010000": "22", "110000": "23",
24
+ "001000": "24", "101000": "25", "011000": "26", "111000": "27",
25
+ "000100": "28", "100100": "29", "010100": "2A", "110100": "2B",
26
+ "001100": "2C", "101100": "2D", "011100": "2E", "111100": "2F",
27
+ "000010": "30", "100010": "31", "010010": "32", "110010": "33",
28
+ "001010": "34", "101010": "35", "011010": "36", "111010": "37",
29
+ "000110": "38", "100110": "39", "010110": "3A", "110110": "3B",
30
+ "001110": "3C", "101110": "3D", "011110": "3E", "111110": "3F",
31
+ "000001": "60", "100001": "61", "010001": "62", "110001": "63",
32
+ "001001": "64", "101001": "65", "011001": "66", "111001": "67",
33
+ "000101": "68", "100101": "69", "010101": "6A", "110101": "6B",
34
+ "001101": "6C", "101101": "6D", "011101": "6E", "111101": "6F",
35
+ "000011": "70", "100011": "71", "010011": "72", "110011": "73",
36
+ "001011": "74", "101011": "75", "011011": "76", "111011": "77",
37
+ "000111": "78", "100111": "79", "010111": "7A", "110111": "7B",
38
+ "001111": "7C", "101111": "7D", "011111": "7E", "111111": "7F"
39
+ }
40
+
41
+ color_codes = {
42
+ (0, 0, 0): ("1B40", "1B50"),
43
+ (255, 0, 0): ("1B41", "1B51"),
44
+ (0, 255, 0): ("1B42", "1B52"),
45
+ (255, 255, 0): ("1B43", "1B53"),
46
+ (0, 0, 255): ("1B44", "1B54"),
47
+ (255, 0, 255): ("1B45", "1B55"),
48
+ (0, 255, 255): ("1B46", "1B56"),
49
+ (255, 255, 255): ("1B47", "1B57")
50
+ }
51
+
52
+
53
+ minitel_color_names = {
54
+ "Black": (0, 0, 0),
55
+ "Red": (255, 0, 0),
56
+ "Green": (0, 255, 0),
57
+ "Yellow": (255, 255, 0),
58
+ "Blue": (0, 0, 255),
59
+ "Magenta": (255, 0, 255),
60
+ "Cyan": (0, 255, 255),
61
+ "White": (255, 255, 255)
62
+ }
63
+
64
+ class MinitelImage(MinitelController):
65
+
66
+ def _convert_image_to_minitel_palette(self, image):
67
+ image = image.convert("RGB")
68
+ pixels = image.load()
69
+ for y in range(image.height):
70
+ for x in range(image.width):
71
+ original_color = pixels[x, y]
72
+ closest_color = min(minitel_palette,
73
+ key=lambda color: sum((color[i] - original_color[i]) ** 2 for i in range(3)))
74
+ pixels[x, y] = closest_color
75
+ return image
76
+
77
+ def _get_preview_image(self, filepath, mode="resize", bg_color=(0, 0, 0)):
78
+ image = Image.open(filepath)
79
+ image = image.convert("RGB")
80
+ target_width, target_height = 80, 72
81
+
82
+ if mode == "resize":
83
+ image = image.resize((target_width, target_height), Image.LANCZOS)
84
+ elif mode == "center":
85
+ iw, ih = image.size
86
+ if iw > target_width or ih > target_height:
87
+ scale_factor = min(target_width / iw, target_height / ih)
88
+ new_width = int(iw * scale_factor)
89
+ new_height = int(ih * scale_factor)
90
+ image = image.resize((new_width, new_height), Image.LANCZOS)
91
+ iw, ih = image.size
92
+ new_img = Image.new("RGB", (target_width, target_height), bg_color)
93
+ left = (target_width - iw) // 2
94
+ top = (target_height - ih) // 2
95
+ new_img.paste(image, (left, top))
96
+ image = new_img
97
+
98
+ image = self._convert_image_to_minitel_palette(image)
99
+ return image
100
+
101
+ def _image_to_g1_row(self, filepath, mode="resize", bg_color=(0, 0, 0)):
102
+ image = self._get_preview_image(filepath, mode, bg_color)
103
+ mosaic_hex_list = []
104
+ mosaic_hex = ""
105
+ target_width, target_height = 80, 72
106
+ for y in range(0, target_height, 3):
107
+ for x in range(0, target_width, 2):
108
+ block_pixels = [
109
+ image.getpixel((x, y)),
110
+ image.getpixel((x + 1, y)),
111
+ image.getpixel((x, y + 1)),
112
+ image.getpixel((x + 1, y + 1)),
113
+ image.getpixel((x, y + 2)),
114
+ image.getpixel((x + 1, y + 2))
115
+ ]
116
+ color_count = {}
117
+ for pixel in block_pixels:
118
+ color_count[pixel] = color_count.get(pixel, 0) + 1
119
+ sorted_colors = sorted(color_count.items(), key=lambda item: item[1], reverse=True)
120
+ bg, fg = sorted_colors[0][0], (sorted_colors[1][0] if len(sorted_colors) > 1 else (255, 255, 255))
121
+ if bg == fg:
122
+ fg = (0, 0, 0) if bg != (0, 0, 0) else (255, 255, 255)
123
+ binary_string = ''.join(['1' if pixel == fg else '0' for pixel in block_pixels])
124
+ g1_code = pixel_to_g1_code.get(binary_string, "7F")
125
+ cell = color_codes[bg][1] + color_codes[fg][0] + g1_code
126
+ mosaic_hex += cell
127
+ mosaic_hex_list.append(mosaic_hex)
128
+ mosaic_hex = ""
129
+ return mosaic_hex_list
130
+
131
+ def _image_to_g1(self, filepath, mode="resize", bg_color=(0, 0, 0)):
132
+ image = self._get_preview_image(filepath, mode, bg_color)
133
+ mosaic_hex = ""
134
+ target_width, target_height = 80, 72
135
+ for y in range(0, target_height, 3):
136
+ for x in range(0, target_width, 2):
137
+ block_pixels = [
138
+ image.getpixel((x, y)),
139
+ image.getpixel((x + 1, y)),
140
+ image.getpixel((x, y + 1)),
141
+ image.getpixel((x + 1, y + 1)),
142
+ image.getpixel((x, y + 2)),
143
+ image.getpixel((x + 1, y + 2))
144
+ ]
145
+ color_count = {}
146
+ for pixel in block_pixels:
147
+ color_count[pixel] = color_count.get(pixel, 0) + 1
148
+ sorted_colors = sorted(color_count.items(), key=lambda item: item[1], reverse=True)
149
+ bg, fg = sorted_colors[0][0], (sorted_colors[1][0] if len(sorted_colors) > 1 else (255, 255, 255))
150
+ if bg == fg:
151
+ fg = (0, 0, 0) if bg != (0, 0, 0) else (255, 255, 255)
152
+ binary_string = ''.join(['1' if pixel == fg else '0' for pixel in block_pixels])
153
+ g1_code = pixel_to_g1_code.get(binary_string, "7F")
154
+ cell = color_codes[bg][1] + color_codes[fg][0] + g1_code
155
+ mosaic_hex += cell
156
+ return mosaic_hex
157
+
158
+ def display_image(self, filepath: str, mode="resize", bg_color=(0, 0, 0)) -> None:
159
+ """
160
+ Display image
161
+ :param filepath: path to image
162
+ :param mode: center or resize
163
+ :param bg_color: background color (default is black)
164
+ """
165
+
166
+ self._write_byte(b'\x1B\x3B\x60\x58\x52')
167
+ self.cursor_off()
168
+ self.clear_screen()
169
+ self.switch_to_g1()
170
+ self._write_byte(b'\x1B\x3A\x6A\x43')
171
+
172
+ bg_color = minitel_color_names.get(bg_color, (0, 0, 0)) if mode == "center" else (0, 0, 0)
173
+
174
+ mosaic_hex = self._image_to_g1(filepath, mode, bg_color)
175
+ data_bytes = bytes.fromhex(mosaic_hex)
176
+ self._write_byte(data_bytes)
177
+
178
+
179
+ def show_image_buggy(self, filepath: str, mode="resize", bg_color=(0, 0, 0)):
180
+ """
181
+ Display image in a buggy way (will animate forever)
182
+ :param filepath: path to image
183
+ :param mode: center or resize
184
+ :param bg_color: background color (default is black)
185
+ """
186
+
187
+ self._write_byte(b'\x1B\x3B\x60\x58\x52')
188
+ self.cursor_off()
189
+ self.clear_screen()
190
+ self.switch_to_g1()
191
+ self._write_byte(b'\x1B\x3A\x6A\x43')
192
+
193
+ bg_color = minitel_color_names.get(bg_color, (0, 0, 0)) if mode == "center" else (0, 0, 0)
194
+
195
+ mosaic_hex = self._image_to_g1_row(filepath, mode, bg_color)
196
+ data_bytes = [bytes.fromhex(mosaic) for mosaic in mosaic_hex]
197
+ for line in data_bytes:
198
+ self._write_byte(line)
199
+
200
+ queue = []
201
+
202
+ empty = b'\x1bP\x1bG '
203
+
204
+ while True:
205
+ if len(queue) < 20:
206
+ x_max_length = 12
207
+
208
+ y = pseudo_random(2, len(data_bytes) - 2)
209
+ x_start = pseudo_random(5, 40 - x_max_length)
210
+ x_length = pseudo_random(4, x_max_length, 2)
211
+
212
+ offset = pseudo_random(0, x_length, 3)
213
+
214
+ glitch_instruction = (1+x_start, 1+y, y, 5*(x_start + offset), 5*(x_start+x_length), offset)
215
+ display_instruction = (1+x_start, 1+y, y, 5*(x_start), 5*(x_start+x_length), 0)
216
+
217
+ index = pseudo_random(min(len(queue), 5), len(queue))
218
+ index2 = pseudo_random(index, len(queue))
219
+ queue.insert(index, glitch_instruction)
220
+ queue.insert(index2+1, display_instruction)
221
+
222
+ dequeue_count = 0
223
+
224
+ if len(queue) > 4:
225
+ dequeue_count = 1
226
+ if len(queue) > 10:
227
+ dequeue_count = 4
228
+
229
+ for dequeue in range(dequeue_count):
230
+ x, y, line, fromByte, toByte, offset = queue.pop(0)
231
+ self.cursorMove(x, y)
232
+ for i in range(offset):
233
+ self._write_byte(empty)
234
+ self._write_byte(data_bytes[line][fromByte:toByte])
@@ -0,0 +1,75 @@
1
+ from .C0 import LF, VT, BS, TAB, CR, RS, FF, US, CAN, BEL, ESC
2
+ from .minitelinterfacing import MinitelInterfacing
3
+
4
+ class MinitelVideotex(MinitelInterfacing):
5
+ """
6
+ Basic Minitel Videotex commands
7
+ """
8
+
9
+ def write(self, text: str):
10
+ self._write_byte(bytes(text, encoding="ascii"))
11
+
12
+ def shift_cursor_down(self):
13
+ """
14
+ Move cursor down one time
15
+ """
16
+ self._write_byte(LF)
17
+
18
+ def shift_cursor_up(self):
19
+ """
20
+ Move cursor right one time
21
+ """
22
+ self._write_byte(VT)
23
+
24
+ def shift_cursor_left(self):
25
+ """
26
+ Move cursor left one time
27
+ """
28
+ self._write_byte(BS)
29
+
30
+ def shift_cursor_right(self):
31
+ """
32
+ Move cursor left one time
33
+ """
34
+ self._write_byte(TAB)
35
+
36
+ def cariage_return(self):
37
+ """
38
+ Move cursor to start of the line (sometimes called Origin)
39
+ """
40
+ self._write_byte(CR)
41
+
42
+ def return_to_top_left(self):
43
+ """
44
+ (also called RS) Move cursor to start of first row. Explicit article separator
45
+ """
46
+ self._write_byte(RS)
47
+
48
+ def explicit_article_separator(self):
49
+ """
50
+ (also called US) Explicit article separator
51
+ """
52
+ self._write_byte(US)
53
+
54
+ def complete_to_end_of_line(self):
55
+ """
56
+ (also called CAN) Write spaces from cursor to end of line (doesn't move cursor)
57
+ """
58
+ self._write_byte(CAN)
59
+
60
+ def clear(self): # Clear screen, go to top left, reset attributes
61
+ self._write_byte(FF)
62
+
63
+ def ring_bel(self) -> None:
64
+ """
65
+ Do a quick sound
66
+ """
67
+ self._write_byte(BEL)
68
+
69
+ def ask_cursor_pos(self):
70
+ """
71
+ Ask current cursor position
72
+ """
73
+ self._write_byte(ESC)
74
+ self._write_byte(b'\x61')
75
+ # TODO : listen to anwser
@@ -0,0 +1,68 @@
1
+ from .C0 import ESC
2
+ from .C1 import DOUBLE_HAUTEUR, DOUBLE_GRANDEUR, DOUBLE_LARGEUR, GRANDEUR_NORMALE, CLIGNOTEMENT, FIXE, FOND_INVERSE, \
3
+ FOND_NORMAL
4
+ from .minitel_csi import MinitelCSI
5
+
6
+
7
+ class MinitelVideotexAttributes(MinitelCSI):
8
+ """
9
+ Minitel Videotex attributes
10
+ """
11
+
12
+ def _set_attribute(self, attribute) -> None:
13
+ """
14
+ Set attribute
15
+ :param attribute: C1 attribute
16
+ """
17
+ self._write_byte(ESC)
18
+ self._write_byte(attribute)
19
+ if attribute == DOUBLE_HAUTEUR or attribute == DOUBLE_GRANDEUR:
20
+ self.move_cursor_down()
21
+
22
+ def set_grandeur_normale(self) -> None:
23
+ """
24
+ Switch to normal-sized characters
25
+ """
26
+ self._set_attribute(GRANDEUR_NORMALE)
27
+
28
+ def set_double_hauteur(self) -> None:
29
+ """
30
+ Switch to double row characters
31
+ """
32
+ self._set_attribute(DOUBLE_HAUTEUR)
33
+
34
+ def set_double_largeur(self) -> None:
35
+ """
36
+ Switch to double column characters
37
+ """
38
+ self._set_attribute(DOUBLE_LARGEUR)
39
+
40
+ def set_double_grandeur(self) -> None:
41
+ """
42
+ Switch to double row and column characters
43
+ """
44
+ self._set_attribute(DOUBLE_GRANDEUR)
45
+
46
+ def set_blinking(self) -> None:
47
+ """
48
+ Switch to blinking characters
49
+ """
50
+ self._set_attribute(CLIGNOTEMENT)
51
+
52
+ def set_non_blinking(self) -> None:
53
+ """
54
+ Revert to non-blinking characters
55
+ """
56
+ self._set_attribute(FIXE)
57
+
58
+ def set_inverted_color(self) -> None:
59
+ """
60
+ Switch to inverted color characters
61
+ """
62
+ self._set_attribute(FOND_INVERSE)
63
+
64
+ def set_non_inverted_color(self) -> None:
65
+ """
66
+ Revert to non-inverted color characters
67
+ """
68
+ self._set_attribute(FOND_NORMAL)
@@ -0,0 +1,91 @@
1
+ from .C0 import ESC, Con, Coff, SO, SI
2
+ from .serial_connection import SerialConnection
3
+
4
+ MODE_TELETEL_VIDEOTEX = 0 # Teletel Videotex mode, default when booting Minitel. Use 40 columns display
5
+ MODE_TELETEL_MIXTE = 1 # Teletel Mixte mode, can be prompted by the server. Use 80 columns display
6
+ MODE_TELEINFORMATIQUE = 2 # Ascii mode, can be prompted by the server or user (Fnct + T then A). Use 80 columns display
7
+
8
+ class MinitelInterfacing(SerialConnection):
9
+ """
10
+ Basic Minitel interface
11
+ """
12
+
13
+ mode = MODE_TELETEL_VIDEOTEX
14
+
15
+ def _write_csi(self):
16
+ """
17
+ Send Command Sequence Introducer code, commonly used for prompting command
18
+ :return:
19
+ """
20
+ self._write_byte(ESC)
21
+ self._write_byte(b'\x5B')
22
+
23
+ def _videotex_to_mixte(self):
24
+ """
25
+ Switch from VideoTex to Mixte
26
+ """
27
+ self._write_byte(ESC)
28
+ self._write_byte(b'\x3A')
29
+ self._write_byte(b'\x32')
30
+ self._write_byte(b'\x7D')
31
+ # Expect to receive \x13 \x70 ?
32
+ self.mode = MODE_TELETEL_MIXTE
33
+
34
+ def _mixte_to_videotex(self):
35
+ """
36
+ Switch from Mixte to VideoTex
37
+ """
38
+ self._write_byte(ESC)
39
+ self._write_byte(b'\x3A')
40
+ self._write_byte(b'\x32')
41
+ self._write_byte(b'\x7E')
42
+ # Expect to receive \x13 \x71 ?
43
+ self.mode = MODE_TELETEL_VIDEOTEX
44
+
45
+ def _teletel_to_teleinformatique(self):
46
+ """
47
+ Switch from Teletel (Mixte or VideoTex?) to Teleinformatique
48
+ Can be done by user with Fnct + T then A (or then F for French ASCII)
49
+ """
50
+ self._write_byte(ESC)
51
+ self._write_byte(b'\x3A')
52
+ self._write_byte(b'\x31')
53
+ self._write_byte(b'\x7D')
54
+ # Expect to receive \x1B \x5B \x3F \x7A ?
55
+ self.mode = MODE_TELEINFORMATIQUE
56
+
57
+ def _teleinformatique_to_videotex(self):
58
+ """
59
+ Switch from Teleinformatique to VideoTex
60
+ Can be done by user with Fnct + T then V
61
+ """
62
+ self._write_byte(ESC)
63
+ self._write_byte(b'\x5B')
64
+ self._write_byte(b'\x3F')
65
+ self._write_byte(b'\x7B')
66
+ # Expect to receive \x13 \x5E ?
67
+ self.mode = MODE_TELETEL_VIDEOTEX
68
+
69
+ def cursor_on(self):
70
+ """
71
+ Display cursor
72
+ """
73
+ self._write_byte(Con)
74
+
75
+ def cursor_off(self):
76
+ """
77
+ Hide cursor
78
+ """
79
+ self._write_byte(Coff)
80
+
81
+ def switch_to_g1(self): # Display images
82
+ """
83
+ Switch to G1 character table (used for semi graphic display)
84
+ """
85
+ self._write_byte(SO)
86
+
87
+ def revert_to_g0(self): # Revert to standard alphanum characters
88
+ """
89
+ Revert to G0 character table (alphanumeric character table)
90
+ """
91
+ self._write_byte(SI)
@@ -0,0 +1,30 @@
1
+ import serial
2
+
3
+ class SerialConnection:
4
+ """
5
+ Raw SerialConnection interface
6
+ """
7
+
8
+ def __init__(self, port: str = '/dev/ttyUSB0', baudrate=4800):
9
+ self.ser = serial.Serial(port, baudrate=baudrate, bytesize=7, parity=serial.PARITY_EVEN, stopbits=serial.STOPBITS_ONE)
10
+
11
+ def __enter__(self):
12
+ return self
13
+
14
+ def __exit__(self, exc_type, exc_val, exc_tb):
15
+ self.ser.close()
16
+
17
+ def _write_byte(self, byte: bytes):
18
+ """
19
+ Send byte over serial connection
20
+ :param byte: Bytes to send
21
+ """
22
+ self.ser.write(byte)
23
+
24
+
25
+ def _write_bytes_p(self, n: int):
26
+ if n <= 9:
27
+ self._write_byte((0x30 + n).to_bytes(1, "big"))
28
+ else :
29
+ self._write_byte((0x30 + (n // 10)).to_bytes(1, "big"))
30
+ self._write_byte((0x30 + (n % 10)).to_bytes(1, "big"))
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: miniminitel
3
+ Version: 1.0.3
4
+ Summary: Simple Minitel interface in Python
5
+ Author: Portevent
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Portevent/MiniMinitel
8
+ Project-URL: Issues, https://github.com/Portevent/MiniMinitel/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: pillow
13
+ Requires-Dist: pyserial
14
+
15
+ # MiniMinitel
16
+ MiniMinitel is a simple Minitel interface in python. It has been created with the help of :
17
+ - http://543210.free.fr/TV/stum1b.pdf
18
+ - https://wiki-ima.plil.fr/mediawiki/images/b/b1/Minitel.pdf
19
+ - https://github.com/eserandour/Minitel1B_Hard
20
+
21
+ ```python
22
+ from miniminitel import MiniMinitel
23
+
24
+ with MiniMinitel("COM4") as minitel:
25
+ minitel.clear()
26
+
27
+ minitel.cursor_move_to(4, 4)
28
+ minitel.write_double_grandeur("Hello world")
29
+
30
+ minitel.write_at(6, 6, "I'm a MiniMinitel")
31
+ minitel.set_blinking()
32
+ minitel.write_at(6, 7, "And I look cool")
33
+ minitel.set_non_blinking()
34
+
35
+ ```
36
+
37
+ # Setup
38
+ ## Python
39
+ MiniMinitel is a work in progress, no version are published to PyPI yet.
40
+ Library is written with Python 3.10, no support for other version are guaranteed (should be compatible Python 3.11+)
41
+ ```commandline
42
+ pip install miniminitel
43
+ ```
44
+
45
+ ## Minitel
46
+ Minitel need to be pluggged to computer throught serial port. Documentation need to be written
@@ -0,0 +1,19 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/miniminitel/C0.py
4
+ src/miniminitel/C1.py
5
+ src/miniminitel/__init__.py
6
+ src/miniminitel/miniminitel.py
7
+ src/miniminitel/minitel_controller.py
8
+ src/miniminitel/minitel_csi.py
9
+ src/miniminitel/minitel_dialog_code.py
10
+ src/miniminitel/minitel_image.py
11
+ src/miniminitel/minitel_videotex.py
12
+ src/miniminitel/minitel_videotex_attributes.py
13
+ src/miniminitel/minitelinterfacing.py
14
+ src/miniminitel/serial_connection.py
15
+ src/miniminitel.egg-info/PKG-INFO
16
+ src/miniminitel.egg-info/SOURCES.txt
17
+ src/miniminitel.egg-info/dependency_links.txt
18
+ src/miniminitel.egg-info/requires.txt
19
+ src/miniminitel.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ pillow
2
+ pyserial
@@ -0,0 +1 @@
1
+ miniminitel