@thlg057/mo5-rag-mcp 1.0.0

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,407 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ makefd.py - Génère une image disquette .fd bootable pour Thomson MO5
4
+ Remplace : fdfs -addBL output.fd BOOTMO.BIN program.BIN
5
+
6
+ Usage:
7
+ python3 makefd.py output.fd program.BIN [file2.BIN ...]
8
+
9
+ Basé sur fdfs.c d'OlivierP-To8 (https://github.com/OlivierP-To8/BootFloppyDisk)
10
+ Le binaire BOOTMO.BIN est embarqué directement dans ce script.
11
+ """
12
+
13
+ import sys
14
+ import struct
15
+ import os
16
+ import time
17
+ from pathlib import Path
18
+
19
+ # ==============================================================================
20
+ # Constantes du format disquette Thomson MO5
21
+ # ==============================================================================
22
+ SECTOR_SIZE = 256 # octets par secteur (255 utiles)
23
+ SECTOR_BYTES = 255 # octets utiles par secteur
24
+ TRACK_SIZE = 16 * SECTOR_SIZE # 16 secteurs par piste
25
+ DISK_SIZE = 80 * TRACK_SIZE # 80 pistes
26
+ BLOCK_SIZE = 8 * SECTOR_SIZE # 8 secteurs par bloc
27
+ BLOCK_BYTES = 8 * SECTOR_BYTES # octets utiles par bloc
28
+
29
+ # Piste 20 : FAT + répertoire
30
+ FAT_OFFSET = 20 * TRACK_SIZE + SECTOR_SIZE + 1 # secteur 2 de la piste 20
31
+ DIR_OFFSET = 20 * TRACK_SIZE + 2 * SECTOR_SIZE # secteur 3 de la piste 20
32
+
33
+ FREE_BLOCK = 0xff
34
+ RESERVED_BLOCK = 0xfe
35
+
36
+ # ==============================================================================
37
+ # BOOTMO.BIN embarqué (compilé depuis BootMO.asm d'OlivierP-To8)
38
+ # Chargeur de boot pour Thomson MO5, max 120 octets utiles
39
+ # org $2200
40
+ # ==============================================================================
41
+ BOOTMO_BIN = bytes([
42
+ 0x00, 0x00, 0x58, 0x22, 0x00, # header Thomson: type=0, size=0x0058, addr=0x2200
43
+ 0x10, 0xce, 0x20, 0xcc, # lds #$20CC
44
+ 0x86, 0x20, 0x1f, 0x8b, # lda #$20 / tfr a,dp
45
+ 0x86, 0x00, 0x97, 0x49, # lda #0 / sta <DKDRV
46
+ 0xcc, 0x14, 0x01, # ldd #$1401 (track 20, sector 1)
47
+ 0x8e, 0x23, 0x00, # ldx #$2300 (Buffer_)
48
+ 0xbd, 0x22, 0x47, # jsr ReadSector_
49
+ 0x1f, 0x12, # tfr x,y
50
+ 0x31, 0x2d, # leay 13,y
51
+ 0xbe, 0x23, 0x0c, # ldx Buffer_+12
52
+ 0x30, 0x1b, # Boot_file: leax -5,x
53
+ 0x31, 0x21, # Boot_track: leay 1,y
54
+ 0xec, 0xa1, # ldd ,y++
55
+ 0x81, 0xff, # cmpa #$ff
56
+ 0x27, 0x0e, # beq Boot_exec
57
+ 0xbd, 0x22, 0x47, # Boot_loop: jsr ReadSector_
58
+ 0x30, 0x89, 0x00, 0xff, # leax 255,x
59
+ 0x5c, # incb
60
+ 0xe1, 0xa4, # cmpb ,y
61
+ 0x2e, 0xec, # bgt Boot_track
62
+ 0x20, 0xf2, # bra Boot_loop
63
+ 0x31, 0x3f, # Boot_exec: leay -1,y
64
+ 0xae, 0xa1, # ldx ,y++
65
+ 0x34, 0x20, # pshs y
66
+ 0xad, 0x84, # jsr ,x
67
+ 0x35, 0x20, # puls y
68
+ 0xae, 0xa0, # ldx ,y+
69
+ 0x8c, 0xff, 0xff, # cmpx #$ffff
70
+ 0x26, 0xd7, # bne Boot_file
71
+ 0x3f, 0x28, # swi / fcb DKBOOT (reboot)
72
+ # ReadSector_:
73
+ 0x34, 0x06, # pshs a,b
74
+ 0x97, 0x4b, # sta <DKTRK
75
+ 0xd7, 0x4c, # stb <DKSEC
76
+ 0x9f, 0x4f, # stx <DKBUF
77
+ 0x86, 0x02, # lda #$02
78
+ 0x97, 0x48, # sta <DKOPC
79
+ 0x3f, 0x26, # swi / fcb DKCO
80
+ 0x35, 0x06, # puls a,b
81
+ 0x39, # rts
82
+ # footer Thomson
83
+ 0xff, 0x00, 0x00, 0x00, 0x00,
84
+ ])
85
+
86
+ # ==============================================================================
87
+ # Classe principale
88
+ # ==============================================================================
89
+ class FloppyDisk:
90
+ def __init__(self):
91
+ self.data = bytearray(DISK_SIZE)
92
+
93
+ def format(self, diskname: str = None):
94
+ """Formate la disquette (équivalent de formatDisk dans fdfs.c)."""
95
+ # Remplissage avec 0xe5
96
+ for i in range(DISK_SIZE):
97
+ self.data[i] = 0xe5
98
+
99
+ # Piste 20 entière initialisée à FREE_BLOCK
100
+ for i in range(20 * TRACK_SIZE, 21 * TRACK_SIZE):
101
+ self.data[i] = FREE_BLOCK
102
+
103
+ # Nom de la disquette sur les 8 premiers octets de la piste 20
104
+ if diskname:
105
+ p = Path(diskname)
106
+ stem = p.stem[:8]
107
+ basename = stem.ljust(8)[:8]
108
+ for i, ch in enumerate(basename):
109
+ self.data[20 * TRACK_SIZE + i] = ord(ch)
110
+
111
+ # Le 1er octet du secteur FAT n'est pas utilisé
112
+ self.data[FAT_OFFSET - 1] = 0x00
113
+
114
+ # Piste 20 réservée (2 blocs)
115
+ self.data[FAT_OFFSET + 2 * 20] = RESERVED_BLOCK
116
+ self.data[FAT_OFFSET + 2 * 20 + 1] = RESERVED_BLOCK
117
+
118
+ # Pistes 80+ à zéro dans la FAT
119
+ for i in range(FAT_OFFSET + 80 * 2, DIR_OFFSET):
120
+ self.data[i] = 0x00
121
+
122
+ # ------------------------------------------------------------------
123
+ def _find_free_block(self, used_blocks: list) -> int:
124
+ """Trouve un bloc libre, en commençant par la fin (piste 79 → 0)."""
125
+ # Blocs déjà référencés dans le répertoire
126
+ in_use = []
127
+ for entry in range(13, 14 * SECTOR_SIZE, 32):
128
+ block = self.data[DIR_OFFSET + entry]
129
+ if block != FREE_BLOCK:
130
+ in_use.append(block)
131
+
132
+ # Balayage de 79 → 0
133
+ for i in range(79, -1, -1):
134
+ if (self.data[FAT_OFFSET + i] == FREE_BLOCK
135
+ and i not in in_use
136
+ and i not in used_blocks):
137
+ return i
138
+ # Puis 80 → 159
139
+ for i in range(80, 2 * 80):
140
+ if (self.data[FAT_OFFSET + i] == FREE_BLOCK
141
+ and i not in in_use
142
+ and i not in used_blocks):
143
+ return i
144
+ return FREE_BLOCK # disquette pleine
145
+
146
+ def _find_free_entry(self) -> int:
147
+ """Trouve une entrée libre dans le répertoire."""
148
+ for i in range(0, 14 * SECTOR_SIZE, 32):
149
+ if self.data[DIR_OFFSET + i] == FREE_BLOCK:
150
+ return i
151
+ return -1
152
+
153
+ def _add_file_entry(self, filename: str, block: int, size_left: int) -> int:
154
+ """Écrit une entrée de répertoire Thomson (32 octets)."""
155
+ entry = DIR_OFFSET + self._find_free_entry()
156
+ name = Path(filename).stem
157
+ ext = Path(filename).suffix.lstrip('.')
158
+
159
+ # Nom (8 octets, complété par des espaces)
160
+ for i in range(8):
161
+ self.data[entry + i] = ord(name[i]) if i < len(name) else 0x20
162
+
163
+ # Extension (3 octets)
164
+ for i in range(3):
165
+ self.data[entry + 8 + i] = ord(ext[i]) if i < len(ext) else 0x20
166
+
167
+ # Type : 2 = programme assembleur (.BIN, .CHG, .MAP), 0 = BASIC sinon
168
+ upper_ext = Path(filename).suffix.upper()
169
+ file_type = 2 if upper_ext in ('.BIN', '.CHG', '.MAP') else 0
170
+ self.data[entry + 11] = file_type
171
+
172
+ # Type données : 0 = binaire
173
+ self.data[entry + 12] = 0x00
174
+
175
+ # Premier bloc
176
+ self.data[entry + 13] = block
177
+
178
+ # Nombre d'octets dans le dernier secteur (big-endian)
179
+ self.data[entry + 14] = (size_left >> 8) & 0xff
180
+ self.data[entry + 15] = size_left & 0xff
181
+
182
+ # Commentaire (1 octet nul + 7 espaces)
183
+ self.data[entry + 16] = 0x00
184
+ for i in range(1, 8):
185
+ self.data[entry + 16 + i] = 0x20
186
+
187
+ # Date (jour, mois, année sur 2 chiffres)
188
+ t = time.localtime()
189
+ self.data[entry + 24] = t.tm_mday
190
+ self.data[entry + 25] = t.tm_mon
191
+ self.data[entry + 26] = t.tm_year - 2000
192
+ for i in range(3, 8):
193
+ self.data[entry + 24 + i] = 0x00
194
+
195
+ return entry
196
+
197
+ def _write_block(self, block: int, file_bytes: bytes, file_size: int, offset: int):
198
+ """Écrit jusqu'à 8 secteurs de données dans un bloc."""
199
+ for b in range(8):
200
+ src = offset + b * SECTOR_BYTES
201
+ dst = block * BLOCK_SIZE + b * SECTOR_SIZE
202
+ nb = SECTOR_BYTES
203
+ if src + SECTOR_BYTES > file_size:
204
+ nb = file_size - src
205
+ if nb > 0:
206
+ self.data[dst:dst + nb] = file_bytes[src:src + nb]
207
+ for j in range(nb, SECTOR_SIZE):
208
+ self.data[dst + j] = 0x00
209
+
210
+ def add_file_content(self, filename: str, file_bytes: bytes):
211
+ """Alloue des blocs et écrit le contenu d'un fichier sur la disquette."""
212
+ size = len(file_bytes)
213
+ blocks = []
214
+ offset = 0
215
+ size_left = size
216
+
217
+ while size_left > 0:
218
+ block = self._find_free_block(blocks)
219
+ self._write_block(block, file_bytes, size, offset)
220
+ offset += BLOCK_BYTES
221
+
222
+ if blocks:
223
+ self.data[FAT_OFFSET + blocks[-1]] = block
224
+ blocks.append(block)
225
+
226
+ if size_left < BLOCK_BYTES:
227
+ # Dernier bloc : calcul du nombre de secteurs occupés
228
+ nb_sectors = 0xc0
229
+ remaining = size_left
230
+ while remaining > 0:
231
+ nb_sectors += 1
232
+ remaining -= SECTOR_BYTES
233
+ self.data[FAT_OFFSET + blocks[-1]] = nb_sectors
234
+ actual_size_left = size_left - SECTOR_BYTES * (nb_sectors - 0xc0 - 1)
235
+ if actual_size_left <= 0:
236
+ actual_size_left = size_left % SECTOR_BYTES
237
+ if actual_size_left == 0:
238
+ actual_size_left = SECTOR_BYTES
239
+ self._add_file_entry(filename, blocks[0], actual_size_left)
240
+
241
+ size_left -= BLOCK_BYTES
242
+
243
+ def add_file(self, filepath: str):
244
+ """
245
+ Charge un fichier .BIN et l'ajoute à la disquette.
246
+ Ajoute le header/footer Thomson si absent.
247
+ """
248
+ filename = os.path.basename(filepath)
249
+ with open(filepath, 'rb') as f:
250
+ raw = f.read()
251
+
252
+ size = len(raw)
253
+ file_bytes = bytearray(raw)
254
+
255
+ if filepath.upper().endswith('.BIN'):
256
+ # Vérifie la présence du header Thomson (5 octets) et footer (5 octets)
257
+ if size >= 10:
258
+ size_cont = size - 10
259
+ has_header = (raw[0] == 0x00
260
+ and raw[1] == (size_cont >> 8) & 0xff
261
+ and raw[2] == size_cont & 0xff)
262
+ else:
263
+ has_header = False
264
+
265
+ if not has_header:
266
+ print(f" /!\\ Pas de header Thomson dans {filename}, ajout automatique impossible sans adresse.")
267
+ print(f" Utilisez la syntaxe CMOC qui génère un .BIN avec header.")
268
+ # On laisse passer tel quel (CMOC génère un BIN avec header)
269
+
270
+ print(f" Ajout de {filename} ({size} octets)")
271
+ self.add_file_content(filename, bytes(file_bytes))
272
+
273
+ # ------------------------------------------------------------------
274
+ def add_boot_loader(self, nb_files: int):
275
+ """
276
+ Écrit le boot loader dans le secteur 1 de la piste 20,
277
+ et les descripteurs de fichiers (adresse, pistes/secteurs) à partir de l'octet 12.
278
+ Équivalent de addBootLoader() dans fdfs.c.
279
+ """
280
+ boot = BOOTMO_BIN
281
+ size = len(boot)
282
+
283
+ if size > 130:
284
+ raise ValueError(f"BOOTMO.BIN trop grand ({size} octets, max 120 utiles)")
285
+
286
+ # Vérification de l'adresse de boot : doit commencer à $6200 ou $2200
287
+ if not ((boot[0] == 0x00)
288
+ and (boot[3] in (0x62, 0x22))
289
+ and (boot[4] == 0x00)):
290
+ raise ValueError("BOOTMO.BIN invalide : doit commencer à l'adresse $6200 ou $2200")
291
+
292
+ # Écriture du secteur de boot (sector 1, track 0, position 0 dans le .fd)
293
+ # On réinitialise les 256 premiers octets du fichier
294
+ for i in range(SECTOR_SIZE):
295
+ self.data[i] = 0x00
296
+
297
+ checksum = 0x55
298
+ content_size = size - 10 # sans header ni footer
299
+ for i in range(content_size):
300
+ self.data[i] = (256 - boot[i + 5]) & 0xff
301
+ checksum = (checksum + boot[i + 5]) & 0xff
302
+
303
+ # Signature "BASIC2" à l'offset 120
304
+ for i, ch in enumerate(b'BASIC2'):
305
+ self.data[120 + i] = ch
306
+
307
+ checksum = (checksum + 0x6c) & 0xff
308
+ self.data[127] = checksum
309
+
310
+ # Marque le bloc 0 comme réservé dans la FAT
311
+ self.data[FAT_OFFSET] = RESERVED_BLOCK
312
+
313
+ # Construction des descripteurs de fichiers dans la piste 20, à partir de l'octet 12
314
+ n = 12
315
+ base = 20 * TRACK_SIZE
316
+
317
+ for f_idx in range(nb_files):
318
+ entry = f_idx * 32
319
+
320
+ block = self.data[DIR_OFFSET + entry + 13]
321
+
322
+ # Lecture du header Thomson du fichier pour extraire adresse de chargement
323
+ file_addr = (self.data[block * BLOCK_SIZE + 3] << 8) | self.data[block * BLOCK_SIZE + 4]
324
+ file_size = (self.data[block * BLOCK_SIZE + 1] << 8) | self.data[block * BLOCK_SIZE + 2]
325
+
326
+ print(f" Fichier #{f_idx}: addr=${file_addr:04x} taille={file_size} octets")
327
+
328
+ # Adresse de chargement (2 octets)
329
+ self.data[base + n] = (file_addr >> 8) & 0xff
330
+ n += 1
331
+ self.data[base + n] = file_addr & 0xff
332
+ n += 1
333
+
334
+ # Parcours des blocs du fichier
335
+ file_exec = 0
336
+ current_block = block
337
+ src = 0
338
+
339
+ while current_block != FREE_BLOCK:
340
+ next_b = self.data[FAT_OFFSET + current_block]
341
+ nbs = 8
342
+ if next_b > 0xc0:
343
+ nbs = next_b - 0xc0
344
+ next_b = FREE_BLOCK
345
+ # Récupère l'adresse d'exécution dans le footer Thomson
346
+ nb_bytes = (self.data[DIR_OFFSET + entry + 14] << 8) | self.data[DIR_OFFSET + entry + 15]
347
+ src = current_block * BLOCK_SIZE + (nbs - 1) * SECTOR_SIZE + nb_bytes
348
+ if nb_bytes < 2:
349
+ file_exec = self.data[src - 3]
350
+ else:
351
+ file_exec = self.data[src - 2]
352
+ file_exec = (file_exec << 8) | self.data[src - 1]
353
+ print(f" exec=${file_exec:04x}")
354
+
355
+ track = current_block >> 1
356
+ sector = 9 if (current_block & 0x01) else 1
357
+
358
+ self.data[base + n] = track; n += 1
359
+ self.data[base + n] = sector; n += 1
360
+ self.data[base + n] = sector + nbs - 1; n += 1
361
+
362
+ current_block = next_b
363
+
364
+ # Marqueur de fin de fichier + adresse d'exécution
365
+ self.data[base + n] = FREE_BLOCK; n += 1
366
+ self.data[base + n] = (file_exec >> 8) & 0xff; n += 1
367
+ self.data[base + n] = file_exec & 0xff; n += 1
368
+
369
+ # ------------------------------------------------------------------
370
+ def save(self, path: str):
371
+ with open(path, 'wb') as f:
372
+ f.write(self.data)
373
+ print(f"✓ Image .fd écrite : {path} ({DISK_SIZE} octets)")
374
+
375
+
376
+ # ==============================================================================
377
+ # Point d'entrée
378
+ # ==============================================================================
379
+ def main():
380
+ if len(sys.argv) < 3:
381
+ print("Usage: python3 makefd.py output.fd program.BIN [file2.BIN ...]")
382
+ print("")
383
+ print(" Génère une image disquette .fd bootable pour Thomson MO5.")
384
+ print(" Remplace : fdfs -addBL output.fd BOOTMO.BIN program.BIN")
385
+ print(" Le boot loader MO5 est embarqué dans ce script.")
386
+ sys.exit(1)
387
+
388
+ output_fd = sys.argv[1]
389
+ input_bins = sys.argv[2:]
390
+
391
+ print(f"=== Génération de {output_fd} ===")
392
+ disk = FloppyDisk()
393
+ disk.format(output_fd)
394
+
395
+ print("--- Ajout des fichiers ---")
396
+ for bin_path in input_bins:
397
+ disk.add_file(bin_path)
398
+
399
+ print("--- Écriture du boot loader MO5 ---")
400
+ disk.add_boot_loader(len(input_bins))
401
+
402
+ os.makedirs(os.path.dirname(os.path.abspath(output_fd)), exist_ok=True)
403
+ disk.save(output_fd)
404
+
405
+
406
+ if __name__ == '__main__':
407
+ main()