{"id":11581,"date":"2026-11-05T00:57:07","date_gmt":"2026-11-04T23:57:07","guid":{"rendered":"https:\/\/retrofixer.it\/?page_id=11581"},"modified":"2026-11-05T01:07:45","modified_gmt":"2026-11-05T00:07:45","slug":"xum1541","status":"publish","type":"page","link":"https:\/\/retrofixer.it\/en\/xum1541\/","title":{"rendered":"XUM1541"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"11581\" class=\"elementor elementor-11581\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-eb9c471 e-con-full e-flex e-con e-parent\" data-id=\"eb9c471\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-91864a4 elementor-widget elementor-widget-html\" data-id=\"91864a4\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;600;700&family=Press+Start+2P&family=VT323&display=swap\" rel=\"stylesheet\">\n\n<style>\n.rfxg{--bg:#0f0c24;--bg2:#171338;--panel:#1d1846;--line:#342c74;--text:#eceaff;--muted:#aaa5d8;--c64l:#7c70da;--c64b:#40318d;--mag:#e0398a;--yel:#ffd23f;--grn:#5ad15f;--cya:#3fd8ff;--red:#f94144;font-family:'Inter',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:var(--text);background:var(--bg);line-height:1.6}\n.rfxg *,.rfxg *::before,.rfxg *::after{box-sizing:border-box}\n.rfxg h2,.rfxg h3,.rfxg h4,.rfxg p,.rfxg ul,.rfxg ol{margin:0;padding:0}\n.rfxg h2,.rfxg h3,.rfxg h4{font-family:'Inter',system-ui,sans-serif!important;letter-spacing:0!important;text-transform:none!important;font-weight:700;color:var(--text)}\n.rfxg a{color:var(--cya)}\n.rfxg [hidden]{display:none!important}\n.rfxg .wrap{max-width:1400px;margin:0 auto;padding:0 16px}\n.rfxg.top{padding:36px 0 4px}\n.rfxg.bottom{padding:8px 0 40px}\n.rfxg .hero h2{font-family:'Press Start 2P',monospace!important;font-size:clamp(17px,3vw,27px);line-height:1.45;color:var(--yel);margin-bottom:12px}\n.rfxg .hero p{color:var(--muted);max-width:900px;font-size:16.5px}\n.rfxg .hero p+p{margin-top:8px}\n.rfxg .nav{display:flex;flex-wrap:wrap;gap:8px;margin:18px 0 6px}\n.rfxg .nav a{background:var(--bg2);border:1px solid var(--line);border-radius:999px;padding:5px 13px;font-size:14px;color:var(--text);text-decoration:none}\n.rfxg .nav a:hover{border-color:var(--yel);color:var(--yel)}\n.rfxg .nav a.go{background:var(--grn);border-color:var(--grn);color:#0f0c24;font-weight:700}\n.rfxg .nav a.go:hover{background:#7be27f;color:#0f0c24}\n.rfxg .sec{margin-top:26px;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:20px 22px 22px;scroll-margin-top:90px}\n.rfxg .sec h3{font-size:20px!important;color:var(--yel)!important;margin-bottom:12px;display:flex;align-items:baseline;gap:10px}\n.rfxg .sec h3 .n{font-family:'Press Start 2P',monospace;font-size:13px;color:var(--mag);margin-right:10px}\n.rfxg .sec h4{font-size:16px!important;color:var(--cya)!important;margin:18px 0 6px}\n.rfxg .sec p{margin:0 0 10px;color:#dcd9f7;max-width:980px}\n.rfxg .sec ul,.rfxg .sec ol{margin:0 0 12px 22px;color:#dcd9f7;max-width:980px}\n.rfxg .sec li{margin:4px 0}\n.rfxg .sec li::marker{color:var(--c64l)}\n.rfxg code{font-family:ui-monospace,Consolas,'Courier New',monospace;font-size:.9em;background:var(--bg2);border:1px solid var(--line);padding:0 5px;border-radius:4px;color:var(--yel)}\n.rfxg pre{font-family:ui-monospace,Consolas,'Courier New',monospace!important;font-size:13.5px!important;line-height:1.5!important;background:#0b0920!important;color:#cfcbef!important;border:1px solid var(--line)!important;border-radius:8px!important;padding:12px 14px!important;margin:6px 0 14px!important;overflow:auto;white-space:pre;max-width:980px}\n.rfxg pre code{background:none;border:0;padding:0;color:inherit;font-size:inherit}\n.rfxg table{border-collapse:collapse!important;margin:6px 0 14px!important;font-size:14.5px;min-width:320px;border:0!important}\n.rfxg .tbl{overflow-x:auto;max-width:100%}\n.rfxg th,.rfxg td{border:1px solid var(--line)!important;padding:7px 11px!important;text-align:left!important;vertical-align:top;background:transparent!important;color:#dcd9f7}\n.rfxg th{background:var(--bg2)!important;color:var(--text)!important;font-weight:600}\n.rfxg td.p{font-family:'VT323',monospace;font-size:20px;color:var(--yel);white-space:nowrap}\n.rfxg .note{border-left:4px solid var(--cya);background:#14284a;border-radius:0 8px 8px 0;padding:10px 14px;margin:10px 0 14px;color:#d3ecff;max-width:980px}\n.rfxg .warn{border-left-color:var(--red);background:#3a1426;color:#ffd5d6}\n.rfxg .ok{border-left-color:var(--grn);background:#12301f;color:#c9f5cb}\n.rfxg .note b{color:#fff}\n.rfxg .cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;margin:8px 0 12px}\n.rfxg .card{background:var(--bg2);border:1px solid var(--line);border-radius:8px;padding:12px 14px}\n.rfxg .card h4{margin:0 0 4px!important;color:var(--yel)!important;font-size:15px!important}\n.rfxg .card p{margin:0;font-size:14.5px;color:var(--muted)}\n.rfxg figure{margin:6px 0 14px;max-width:980px}\n.rfxg figure svg{display:block;width:100%;height:auto;background:#0b0920;border:1px solid var(--line);border-radius:8px}\n.rfxg figcaption{font-size:13px;color:var(--muted);margin-top:6px}\n.rfxg .steps{counter-reset:st;list-style:none;margin-left:0!important}\n.rfxg .steps>li{counter-increment:st;position:relative;padding-left:40px;margin:10px 0}\n.rfxg .steps>li::before{content:counter(st);position:absolute;left:0;top:1px;width:28px;height:28px;border-radius:50%;background:var(--c64b);border:1px solid var(--c64l);color:var(--yel);font-family:'Press Start 2P',monospace;font-size:11px;display:flex;align-items:center;justify-content:center}\n.rfxg .credits{margin-top:20px;font-size:13px;color:var(--muted);max-width:1100px}\n@media (max-width:600px){.rfxg .sec{padding:16px 14px 16px}.rfxg .steps>li{padding-left:36px}}\n\/* difese contro gli stili del tema (Betheme) *\/\n.rfxg table{width:auto!important;max-width:100%}\n.rfxg th,.rfxg td{vertical-align:middle!important;font-size:14.5px!important;line-height:1.4!important}\n.rfxg td.p{font-size:20px!important;line-height:1.1!important}\n.rfxg .sec ul{list-style:disc outside!important}\n.rfxg .sec ol{list-style:decimal outside!important}\n.rfxg .sec ol.steps{list-style:none!important}\n.rfxg .sec h3 .n{display:inline-block;margin-right:12px!important}\n.rffl{--bg:#0f0c24;--bg2:#171338;--panel:#1d1846;--line:#342c74;--text:#eceaff;--muted:#aaa5d8;--c64l:#7c70da;--c64b:#40318d;--mag:#e0398a;--yel:#ffd23f;--grn:#5ad15f;--cya:#3fd8ff;--red:#f94144;font-family:'Inter',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:var(--text);background:var(--bg);line-height:1.55;padding:36px 0 28px}\n.rffl *,.rffl *::before,.rffl *::after{box-sizing:border-box}\n.rffl h2,.rffl h3,.rffl h4,.rffl p,.rffl ul,.rffl ol{margin:0;padding:0}\n.rffl h2,.rffl h3,.rffl h4{font-family:'Inter',system-ui,sans-serif!important;letter-spacing:0;text-transform:none;font-weight:700;color:var(--text)}\n.rffl a{color:var(--cya)}\n.rffl button,.rffl input,.rffl select{font:inherit;color:inherit}\n.rffl button{cursor:pointer}\n.rffl button:disabled{cursor:not-allowed;opacity:.45}\n.rffl [hidden]{display:none!important}\n.rffl .wrap{max-width:1400px;margin:0 auto;padding:0 16px}\n.rffl .head h2{font-family:'Press Start 2P',monospace!important;font-size:clamp(17px,3vw,27px);line-height:1.45;color:var(--yel);margin-bottom:12px}\n.rffl .head p{color:var(--muted);max-width:900px;font-size:16px}\n.rffl .head .stats{font-family:'VT323',monospace;font-size:19px;color:var(--c64l);margin-top:8px}\n.rffl .head .stats b{color:var(--grn);font-weight:400}\n.rffl .head .stats i{color:var(--red);font-style:normal}\n\n\/* pulsanti *\/\n.rffl .btn{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);background:var(--bg2);border-radius:8px;padding:7px 12px;font-size:14px;line-height:1.3;white-space:nowrap}\n.rffl .btn:hover:not(:disabled){background:var(--line);color:#fff}\n.rffl .btn.pri{background:var(--grn);border-color:var(--grn);color:#0f0c24;font-weight:700}\n.rffl .btn.pri:hover:not(:disabled){background:#7be27f;color:#0f0c24}\n.rffl .btn.warn{border-color:var(--mag);color:#ff9ccc}\n.rffl .btn.warn:hover:not(:disabled){background:var(--mag);color:#fff}\n.rffl .btn.sm{padding:4px 9px;font-size:13px;border-radius:6px}\n.rffl .btn .i{font-size:15px;line-height:1}\n.rffl input[type=text],.rffl input[type=search],.rffl select{background:var(--bg2);border:1px solid var(--line);border-radius:7px;padding:7px 10px;font-size:14px;outline:none}\n.rffl input[type=text]:focus,.rffl input[type=search]:focus,.rffl select:focus{border-color:var(--c64l)}\n.rffl select option{background:var(--bg2)}\n.rffl label.ck{display:inline-flex;align-items:center;gap:7px;font-size:14px;color:var(--muted);cursor:pointer}\n.rffl label.ck input{accent-color:var(--grn);width:16px;height:16px}\n\n\/* collegamento *\/\n.rffl .conn{margin:22px 0 16px;background:var(--panel);border:1px solid var(--line);border-radius:10px}\n.rffl .modes{display:flex;border-bottom:1px solid var(--line)}\n.rffl .modes button{flex:1;background:none;border:0;border-bottom:3px solid transparent;padding:12px 10px 10px;font-size:15px;font-weight:600;color:var(--muted);display:flex;flex-direction:column;align-items:center;gap:1px}\n.rffl .modes button small{font-weight:400;font-size:12.5px;color:var(--muted)}\n.rffl .modes button[aria-selected=true]{color:#fff;border-bottom-color:var(--yel)}\n.rffl .modes button:hover{color:#fff}\n.rffl .cbody{padding:14px 16px;display:flex;flex-wrap:wrap;gap:10px 14px;align-items:center}\n.rffl .cbody .note{flex:1 1 100%;font-size:14px;color:var(--muted)}\n.rffl .cbody .note b{color:var(--text)}\n.rffl .cbody .note.err{color:#ffb3b4}\n.rffl .fld{display:inline-flex;align-items:center;gap:7px;font-size:14px;color:var(--muted)}\n.rffl details.help{flex:1 1 100%;font-size:14px;color:var(--muted);border-top:1px dashed var(--line);padding-top:8px}\n.rffl details.help summary{cursor:pointer;color:var(--cya);font-weight:600}\n.rffl details.help ul{margin:8px 0 0 18px}\n.rffl details.help li{margin:3px 0}\n.rffl details.help code,.rffl .cbody code,.rffl .credits code{font-family:ui-monospace,Consolas,monospace;font-size:13px;background:var(--bg2);padding:0 4px;border-radius:3px;color:var(--yel)}\n.rffl .devrow{display:flex;flex-wrap:wrap;gap:8px 14px;align-items:center;padding:10px 16px;border-top:1px solid var(--line);background:var(--bg2);border-radius:0 0 10px 10px}\n.rffl .led{width:14px;height:14px;border-radius:50%;background:#3a1416;border:1px solid #000;box-shadow:inset 0 0 3px #000;flex:none}\n.rffl .led.on{background:var(--red);box-shadow:0 0 8px var(--red),0 0 2px #fff inset}\n.rffl .pwr{width:14px;height:14px;border-radius:3px;background:#173a19;border:1px solid #000;flex:none}\n.rffl .pwr.on{background:var(--grn);box-shadow:0 0 7px var(--grn)}\n.rffl .dstat{font-family:'VT323',monospace;font-size:20px;color:var(--c64l);min-width:0;overflow-wrap:anywhere}\n.rffl .dstat.err{color:var(--red)}\n.rffl .dstat.ok{color:var(--grn)}\n.rffl .devrow .sp{flex:1}\n\n\/* layout *\/\n.rffl .main{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;align-items:start}\n.rffl .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;min-width:0;position:relative}\n.rffl .phead{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:baseline;padding:12px 14px 10px;border-bottom:1px solid var(--line)}\n.rffl .phead h3{font-size:17px!important;color:var(--yel)!important}\n.rffl .phead .sub{font-size:13.5px;color:var(--muted)}\n.rffl .tools{display:flex;flex-wrap:wrap;gap:6px;padding:10px 14px}\n.rffl .tools .sep{flex:1}\n.rffl .pbody{padding:0 14px 14px}\n\n\/* indicatore drive reale \/ simulazione *\/\n.rffl .mflag{margin:10px 14px 0;border-radius:8px;padding:9px 12px;font-size:14px;line-height:1.45;border:1px solid var(--line);background:var(--bg2);color:var(--muted)}\n.rffl .mflag b{font-family:'VT323',monospace;font-size:21px;font-weight:400;margin-right:6px;letter-spacing:.03em}\n.rffl .mflag .btn{margin-left:6px;vertical-align:middle}\n.rffl .mflag.on{border-color:var(--grn);background:#12301f;color:#c9f5cb}\n.rffl .mflag.on b{color:var(--grn)}\n.rffl .mflag.off{border-color:#5a5390}\n.rffl .mflag.off b{color:#ff8c8e}\n.rffl .mflag.sim{border:1px dashed var(--mag);background:repeating-linear-gradient(135deg,#2a1438 0 14px,#24113a 14px 28px);color:#ffd0e6}\n.rffl .mflag.sim b{color:var(--yel)}\n.rffl .panel[data-mode=on]{border-color:var(--grn);box-shadow:0 0 0 1px #2c6a34 inset}\n.rffl .panel[data-mode=sim]{border-style:dashed;border-color:var(--mag)}\n.rffl .modes button[data-m=usb] span:first-child{color:var(--grn)}\n.rffl .modes button[data-m=usb][aria-selected=true]{border-bottom-color:var(--grn)}\n.rffl .modes button[data-m=sim][aria-selected=true]{border-bottom-color:var(--mag)}\n\n\/* avanzamento *\/\n.rffl .prog{margin:0 14px 10px;background:var(--bg2);border:1px solid var(--line);border-radius:8px;padding:9px 11px}\n.rffl .prog .top{display:flex;gap:10px;align-items:center;font-size:14px;margin-bottom:6px}\n.rffl .prog .top span{flex:1;min-width:0}\n.rffl .prog .bar{height:10px;background:#0b0920;border-radius:5px;overflow:hidden}\n.rffl .prog .bar i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--c64l),var(--cya));transition:width .2s}\n.rffl .prog .bar.ind i{width:30%!important;animation:rfflind 1.2s linear infinite}\n@keyframes rfflind{0%{transform:translateX(-100%)}100%{transform:translateX(340%)}}\n.rffl .prog .eta{font-family:'VT323',monospace;font-size:18px;color:var(--c64l);margin-top:4px}\n\n\/* schermo C64 *\/\n.rffl .scr{background:var(--c64b);border:12px solid var(--c64l);border-radius:6px;padding:8px 0;font-family:'VT323',monospace;font-size:21px;line-height:1.05;color:#a8a0ff;min-height:180px;max-height:560px;overflow:auto;scrollbar-width:thin;scrollbar-color:var(--c64l) var(--c64b)}\n.rffl .scr .ln{display:flex;align-items:center;gap:8px;padding:1px 10px;white-space:pre;min-height:24px}\n.rffl .scr .ln .tx{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}\n.rffl .scr .hdr .tx span{background:#a8a0ff;color:var(--c64b)}\n.rffl .scr .row{cursor:pointer;width:100%;text-align:left;background:none;border:0;color:inherit;font:inherit}\n.rffl .scr .row:hover,.rffl .scr .row:focus-visible{background:#5546a8;outline:none}\n.rffl .scr .row.sel{background:#a8a0ff;color:var(--c64b)}\n.rffl .scr .row .acts{display:none;gap:4px;flex:none}\n.rffl .scr .row:hover .acts,.rffl .scr .row.sel .acts,.rffl .scr .row:focus-within .acts{display:flex}\n.rffl .scr .acts span{font-family:'Inter',system-ui,sans-serif;font-size:12.5px;line-height:1.2;background:var(--bg);color:var(--text);border:1px solid var(--line);border-radius:5px;padding:2px 7px;white-space:nowrap}\n.rffl .scr .acts span:hover{background:var(--yel);color:#1a1440;border-color:var(--yel)}\n.rffl .scr .acts span.del:hover{background:var(--red);color:#fff;border-color:var(--red)}\n.rffl .scr .empty{padding:10px;color:#a8a0ff;white-space:normal}\n.rffl .scr .cur{display:inline-block;width:.55em;height:.9em;background:#a8a0ff;vertical-align:-2px;animation:rfflblink 1s steps(1) infinite}\n@keyframes rfflblink{50%{opacity:0}}\n.rffl .drophint{margin-top:10px;border:2px dashed var(--line);border-radius:8px;padding:10px 12px;font-size:14px;color:var(--muted);text-align:center}\n.rffl .panel.over{outline:3px solid var(--yel);outline-offset:-3px}\n.rffl .panel.over .drophint{border-color:var(--yel);color:var(--yel)}\n\n\/* schede del pannello PC *\/\n.rffl .tabs{display:flex;border-bottom:1px solid var(--line);overflow-x:auto;scrollbar-width:none}\n.rffl .tabs button{flex:1 0 auto;background:none;border:0;border-bottom:3px solid transparent;padding:12px 12px 9px;font-size:14px;font-weight:600;color:var(--muted)}\n.rffl .tabs button[aria-selected=true]{color:#fff;border-bottom-color:var(--yel)}\n.rffl .tabs button:hover{color:#fff}\n.rffl .tabs .badge{display:inline-block;min-width:18px;padding:0 5px;margin-left:5px;border-radius:9px;background:var(--line);font-size:11.5px;color:var(--text)}\n.rffl .pane{padding:14px}\n.rffl .drop{border:2px dashed var(--line);border-radius:10px;padding:18px 14px;text-align:center;color:var(--muted);font-size:14.5px;transition:border-color .15s,background .15s}\n.rffl .drop.over{border-color:var(--yel);background:#2a2366;color:#fff}\n.rffl .drop .big{font-size:16px;color:var(--text);font-weight:600;margin-bottom:4px}\n.rffl .drop .btns{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:12px}\n.rffl .flist{margin-top:12px;display:flex;flex-direction:column;gap:6px}\n.rffl .fitem{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:4px 10px;align-items:center;background:var(--bg2);border:1px solid var(--line);border-radius:8px;padding:8px 10px}\n.rffl .fitem .ty{font-family:'VT323',monospace;font-size:18px;color:#1a1440;background:var(--c64l);border-radius:4px;padding:0 6px;text-align:center;min-width:44px}\n.rffl .fitem .ty.img{background:var(--yel)}\n.rffl .fitem .ty.seq{background:var(--cya)}\n.rffl .fitem .nm{min-width:0}\n.rffl .fitem .nm b{display:block;font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.rffl .fitem .nm small{font-size:12.5px;color:var(--muted)}\n.rffl .fitem .c64{grid-column:2\/4;display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:13px;color:var(--muted)}\n.rffl .fitem .c64 input{font-family:'VT323',monospace;font-size:18px;padding:2px 8px;width:15em;max-width:100%;text-transform:uppercase}\n.rffl .fitem .c64 select{padding:3px 6px;font-size:13px}\n.rffl .fitem .fa{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}\n.rffl .listbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:12px;font-size:13.5px;color:var(--muted)}\n.rffl .listbar .sp{flex:1}\n.rffl .none{color:var(--muted);padding:14px 4px;font-size:14.5px}\n\n\/* visualizzatore immagine *\/\n.rffl .vhead{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:baseline;margin-bottom:10px}\n.rffl .vhead b{font-size:15px}\n.rffl .vhead span{font-size:13.5px;color:var(--muted)}\n.rffl .vtools{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}\n.rffl .bam{margin-top:12px;background:var(--bg2);border:1px solid var(--line);border-radius:8px;padding:10px}\n.rffl .bam h4{font-size:13px!important;font-weight:600;color:var(--muted);text-transform:uppercase!important;letter-spacing:.06em!important;margin-bottom:8px}\n.rffl .bam canvas{display:block;width:100%;height:auto;image-rendering:pixelated;cursor:crosshair}\n.rffl .bam .leg{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:12.5px;color:var(--muted);margin-top:8px}\n.rffl .bam .leg i{display:inline-block;width:11px;height:11px;border-radius:2px;margin-right:5px;vertical-align:-1px}\n.rffl .bam .tip{font-family:'VT323',monospace;font-size:18px;color:var(--cya);min-height:22px;margin-top:4px}\n.rffl .detail{margin-top:12px;background:var(--bg2);border:1px solid var(--line);border-radius:8px;padding:12px}\n.rffl .detail .dh{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center;margin-bottom:8px}\n.rffl .detail .dh b{font-family:'VT323',monospace;font-size:22px;color:var(--yel);font-weight:400}\n.rffl .detail .dh span{font-size:13.5px;color:var(--muted)}\n.rffl .detail .dh .sp{flex:1}\n.rffl .seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}\n.rffl .seg button{background:var(--bg2);border:0;padding:5px 11px;font-size:13px;color:var(--muted)}\n.rffl .seg button+button{border-left:1px solid var(--line)}\n.rffl .seg button[aria-pressed=true]{background:var(--line);color:#fff}\n.rffl pre.code{font-family:'VT323',monospace;font-size:19px;line-height:1.15;background:var(--c64b);color:#a8a0ff;border:1px solid #6c5eb5;border-radius:6px;padding:10px 12px;margin:8px 0 0;max-height:420px;overflow:auto;white-space:pre;scrollbar-width:thin}\n.rffl pre.hex{font-family:ui-monospace,Consolas,'Courier New',monospace;font-size:12.5px;line-height:1.45;background:#0b0920;color:#cfcbef;border:1px solid var(--line);border-radius:6px;padding:10px 12px;margin:8px 0 0;max-height:420px;overflow:auto;white-space:pre}\n.rffl pre.hex i{color:var(--c64l);font-style:normal}\n.rffl pre.hex u{color:var(--muted);text-decoration:none}\n\n\/* CSDb *\/\n.rffl .csform{display:flex;gap:8px}\n.rffl .csform input{flex:1;min-width:0}\n.rffl .csnote{font-size:13px;color:var(--muted);margin-top:8px}\n.rffl .csres{margin-top:12px;display:flex;flex-direction:column}\n.rffl .csres button.hit{display:flex;flex-wrap:wrap;gap:2px 10px;align-items:baseline;text-align:left;background:none;border:0;border-top:1px solid var(--line);padding:9px 6px}\n.rffl .csres button.hit:hover{background:#231d55}\n.rffl .csres .hit b{font-weight:600;font-size:14.5px}\n.rffl .csres .hit span{font-size:13px;color:var(--muted)}\n.rffl .csres .hit em{font-style:normal;font-family:'VT323',monospace;font-size:18px;color:var(--c64l);margin-left:auto}\n.rffl .rel{margin-top:12px;display:grid;grid-template-columns:160px minmax(0,1fr);gap:12px;background:var(--bg2);border:1px solid var(--line);border-radius:8px;padding:12px}\n.rffl .rel img{width:160px;height:auto;border-radius:4px;background:#000;image-rendering:pixelated}\n.rffl .rel h4{font-size:16px!important;color:var(--yel)!important;margin-bottom:4px}\n.rffl .rel .meta{font-size:13.5px;color:var(--muted);margin-bottom:8px}\n.rffl .rel .dls{display:flex;flex-direction:column;gap:6px}\n.rffl .rel .dl{display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center;font-size:13.5px}\n.rffl .rel .dl code{font-family:ui-monospace,Consolas,monospace;font-size:12.5px;overflow-wrap:anywhere;color:var(--text)}\n.rffl .rel .dl small{color:var(--muted)}\n\n\/* registro *\/\n.rffl .logbar{display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center;margin-bottom:8px}\n.rffl .logbar .sp{flex:1}\n.rffl .log{font-family:ui-monospace,Consolas,'Courier New',monospace;font-size:12.5px;line-height:1.5;background:#0b0920;border:1px solid var(--line);border-radius:6px;padding:8px 10px;height:420px;overflow:auto;white-space:pre-wrap;word-break:break-word}\n.rffl .log div{color:#cfcbef}\n.rffl .log .t{color:var(--muted)}\n.rffl .log .k-usb{color:#8a84b8}\n.rffl .log .k-bus{color:var(--c64l)}\n.rffl .log .k-cmd{color:var(--yel)}\n.rffl .log .k-st{color:var(--grn)}\n.rffl .log .k-err{color:var(--red)}\n.rffl .log .k-info{color:var(--cya)}\n.rffl .log.nobus .k-bus,.rffl .log.nobus .k-usb{display:none}\n\n.rffl .credits{margin-top:18px;font-size:13px;color:var(--muted);max-width:1100px}\n.rffl .credits p+p{margin-top:6px}\n\n\/* finestre di dialogo *\/\n.rffl dialog.dlg{background:var(--panel);color:var(--text);border:1px solid var(--c64l);border-radius:12px;padding:0;width:min(520px,calc(100vw - 32px));box-shadow:0 20px 60px #000}\n.rffl dialog.dlg::backdrop{background:rgb(5 4 15 \/ .8)}\n.rffl .dlg form{padding:18px 18px 16px}\n.rffl .dlg h3{font-size:18px!important;color:var(--yel)!important;margin-bottom:10px}\n.rffl .dlg .body{font-size:14.5px;color:#dcd9f7}\n.rffl .dlg .body p+p{margin-top:8px}\n.rffl .dlg .body .warn{color:#ff9ccc}\n.rffl .dlg .fields{display:flex;flex-direction:column;gap:10px;margin-top:12px}\n.rffl .dlg .fields label{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var(--muted)}\n.rffl .dlg .fields label.ck{flex-direction:row}\n.rffl .dlg .fields input[type=text]{font-family:'VT323',monospace;font-size:20px;padding:4px 10px;text-transform:uppercase}\n.rffl .dlg .fields input.free{text-transform:none}\n.rffl .dlg .btns{display:flex;flex-direction:row-reverse;flex-wrap:wrap-reverse;gap:8px;justify-content:flex-start;margin-top:16px}\n.rffl .dlg .out{font-family:'VT323',monospace;font-size:20px;color:var(--grn);margin-top:10px;min-height:24px}\n.rffl .dlg .out.err{color:var(--red)}\n.rffl .toast{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:99999;background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--grn);border-radius:8px;padding:10px 14px;font-size:14px;box-shadow:0 8px 30px #000;max-width:calc(100vw - 32px)}\n.rffl .toast.err{border-left-color:var(--red)}\n\n@media (max-width:1050px){.rffl .main{grid-template-columns:minmax(0,1fr)}}\n@media (max-width:600px){\n .rffl .modes button{font-size:14px}\n .rffl .scr{font-size:18px;border-width:8px}\n .rffl .scr .acts{display:flex!important}\n .rffl .scr .row:not(.sel) .acts{display:none!important}\n .rffl .scr .row.sel{flex-wrap:wrap}\n .rffl .scr .row.sel .acts{flex-basis:100%;justify-content:flex-end;padding:2px 0 4px}\n .rffl .rel{grid-template-columns:minmax(0,1fr)}\n .rffl .rel img{width:100%;max-width:320px}\n .rffl .fitem{grid-template-columns:auto minmax(0,1fr)}\n .rffl .fitem .fa{grid-column:1\/-1;justify-content:flex-start}\n}\n\n.post-views.post-11581{background:#0f0c24;color:#aaa5d8;margin:0;padding:0 16px 8px;font-size:13px}\n.rfxg.top+.rffl{padding-top:14px}\n.rfxg.top+.rffl .head h2{font-size:clamp(14px,2.2vw,19px);margin-bottom:8px}\n.rffl select,.rffl input[type=text],.rffl input[type=search]{height:auto!important;min-height:0!important;width:auto;margin:0!important;box-shadow:none!important;background:var(--bg2)!important;color:var(--text)!important;border:1px solid var(--line)!important;border-radius:7px!important;padding:7px 10px!important;font-size:14px!important;line-height:1.3!important}\n.rffl .csform input{width:auto!important;flex:1}\n.rffl .fitem .c64 input,.rffl .dlg .fields input[type=text]{font-family:'VT323',monospace!important;font-size:19px!important;padding:3px 9px!important}\n.rffl pre{margin:8px 0 0!important}\n.rffl button{text-transform:none;letter-spacing:normal}\n.rffl .modes button,.rffl .tabs button,.rffl .seg button{border-radius:0!important}\n<\/style>\n\n<div class=\"rfxg top\" data-no-translation>\n <div class=\"wrap\">\n  <div class=\"hero\">\n   <div data-lang=\"it\">\n    <h2>XUM1541: i floppy del Commodore sul PC<\/h2>\n    <p>La XUM1541 \u00e8 un piccolo adattatore USB che collega un drive 1541, 1571 o 1581 a un computer moderno. Con lei puoi salvare in un'immagine D64 i dischi che hai nel cassetto da quarant'anni, prima che si smagnetizzino, oppure scrivere sui floppy veri i giochi e le demo scaricati da internet.<\/p>\n    <p>In questa pagina trovi il programma per usarla direttamente dal browser e la guida completa: com'\u00e8 fatta, come costruirla con un Arduino Pro Micro, come caricare il firmware e installare i driver.<\/p>\n   <\/div>\n   <div data-lang=\"en\" hidden>\n    <h2>XUM1541: Commodore floppies on your PC<\/h2>\n    <p>The XUM1541 is a small USB adapter that connects a 1541, 1571 or 1581 drive to a modern computer. With it you can save the disks that have been sitting in your drawer for forty years into D64 images, before they lose their magnetism, or write games and demos downloaded from the internet onto real floppies.<\/p>\n    <p>This page has the program to use it straight from the browser and the full guide: how it is made, how to build one with an Arduino Pro Micro, how to load the firmware and install the drivers.<\/p>\n   <\/div>\n  <\/div>\n  <nav class=\"nav\" data-lang=\"it\">\n   <a class=\"go\" href=\"#programma\">\u25b6 Il programma<\/a><a href=\"#cose\">Cos'\u00e8<\/a><a href=\"#com-e-fatta\">Com'\u00e8 fatta<\/a><a href=\"#costruirla\">Costruirla<\/a><a href=\"#firmware\">Firmware<\/a><a href=\"#driver\">Driver<\/a><a href=\"#usarla\">Usarla<\/a><a href=\"#opencbm\">OpenCBM<\/a>\n  <\/nav>\n  <nav class=\"nav\" data-lang=\"en\" hidden>\n   <a class=\"go\" href=\"#programma\">\u25b6 The program<\/a><a href=\"#cose\">What it is<\/a><a href=\"#com-e-fatta\">How it's made<\/a><a href=\"#costruirla\">Build it<\/a><a href=\"#firmware\">Firmware<\/a><a href=\"#driver\">Drivers<\/a><a href=\"#usarla\">Use it<\/a><a href=\"#opencbm\">OpenCBM<\/a>\n  <\/nav>\n <\/div>\n <span id=\"programma\"><\/span>\n<\/div>\n<div class=\"rffl\" data-no-translation>\n <div class=\"wrap\">\n  <div class=\"head\">\n   <h2 data-t=\"title\">Floppy C64 dal browser<\/h2>\n   <p data-t=\"intro\"><\/p>\n   <div class=\"stats\" id=\"rfflStats\"><\/div>\n  <\/div>\n\n  <section class=\"conn\" aria-labelledby=\"rfflConnT\">\n   <h3 id=\"rfflConnT\" hidden data-t=\"connT\">Collegamento<\/h3>\n   <div class=\"modes\" role=\"tablist\">\n    <button role=\"tab\" data-m=\"usb\" aria-selected=\"false\"><span data-t=\"mUsb\">xum1541 \/ ZoomFloppy<\/span><small data-t=\"mUsbS\">drive vero via USB<\/small><\/button>\n    <button role=\"tab\" data-m=\"sim\" aria-selected=\"true\"><span data-t=\"mSim\">Drive simulato<\/span><small data-t=\"mSimS\">per provare senza hardware<\/small><\/button>\n   <\/div>\n\n   <div class=\"cbody\" id=\"rfflUsbBox\" hidden>\n    <button class=\"btn pri\" id=\"rfflUsbGo\" type=\"button\"><span class=\"i\">\ud83d\udd0c<\/span><span data-t=\"usbGo\">Collega la xum1541<\/span><\/button>\n    <button class=\"btn\" id=\"rfflUsbAgain\" type=\"button\" hidden data-t=\"usbAgain\">Ricollega l'ultima<\/button>\n    <button class=\"btn\" id=\"rfflUsbOff\" type=\"button\" hidden data-t=\"usbOff\">Scollega<\/button>\n    <p class=\"note\" id=\"rfflUsbNote\"><\/p>\n    <details class=\"help\">\n     <summary data-t=\"helpT\">Prima di collegarla: driver e permessi<\/summary>\n     <ul data-th=\"helpL\"><\/ul>\n    <\/details>\n   <\/div>\n\n   <div class=\"cbody\" id=\"rfflSimBox\">\n    <label class=\"fld\"><span data-t=\"simModel\">Drive<\/span>\n     <select id=\"rfflSimModel\"><option value=\"1541\">1541<\/option><option value=\"1571\">1571<\/option><option value=\"1581\">1581<\/option><\/select><\/label>\n    <label class=\"fld\"><span data-t=\"simDisk\">Disco<\/span>\n     <select id=\"rfflSimDisk\"><option value=\"demo\" data-t=\"dDemo\">dimostrativo<\/option><option value=\"blank\" data-t=\"dBlank\">vuoto, formattato<\/option><option value=\"none\" data-t=\"dNone\">nessun disco<\/option><\/select><\/label>\n    <label class=\"fld\"><span data-t=\"simSpeed\">Velocit\u00e0<\/span>\n     <select id=\"rfflSimSpeed\"><option value=\"instant\" data-t=\"spInst\">istantanea<\/option><option value=\"real\" data-t=\"spReal\">realistica (bus seriale)<\/option><\/select><\/label>\n    <label class=\"ck\"><input type=\"checkbox\" id=\"rfflSimWp\"><span data-t=\"simWp\">disco protetto da scrittura<\/span><\/label>\n    <button class=\"btn\" id=\"rfflSimDl\" type=\"button\"><span class=\"i\">\u2b07<\/span><span data-t=\"simDl\">Scarica il disco simulato<\/span><\/button>\n    <p class=\"note\" data-th=\"simNote\"><\/p>\n   <\/div>\n\n   <div class=\"devrow\">\n    <span class=\"pwr\" id=\"rfflPwr\" title=\"Power\"><\/span>\n    <span class=\"led\" id=\"rfflLed\" title=\"Drive\"><\/span>\n    <label class=\"fld\"><span data-t=\"devNr\">Numero<\/span>\n     <select id=\"rfflDev\"><option>8<\/option><option>9<\/option><option>10<\/option><option>11<\/option><\/select><\/label>\n    <span class=\"dstat\" id=\"rfflDstat\">\u2014<\/span>\n    <span class=\"sp\"><\/span>\n    <button class=\"btn sm\" id=\"rfflSt\" type=\"button\" data-t=\"bStatus\">Leggi stato<\/button>\n    <button class=\"btn sm\" id=\"rfflId\" type=\"button\" data-t=\"bIdent\">Identifica (UJ)<\/button>\n    <button class=\"btn sm\" id=\"rfflRst\" type=\"button\" data-t=\"bReset\">Reset bus<\/button>\n   <\/div>\n  <\/section>\n\n  <div class=\"main\">\n   <section class=\"panel\" id=\"rfflDrv\" aria-labelledby=\"rfflDrvT\">\n    <div class=\"phead\"><h3 id=\"rfflDrvT\" data-t=\"drvT\">Disco nel drive<\/h3><span class=\"sub\" id=\"rfflDrvSub\"><\/span><\/div>\n    <div class=\"mflag\" id=\"rfflFlag\" role=\"status\"><\/div>\n    <div class=\"tools\">\n     <button class=\"btn\" id=\"rfflDir\" type=\"button\"><span class=\"i\">\u21bb<\/span><span data-t=\"bDir\">Directory<\/span><\/button>\n     <button class=\"btn\" id=\"rfflFmt\" type=\"button\" data-t=\"bFmt\">Formatta\u2026<\/button>\n     <button class=\"btn\" id=\"rfflVal\" type=\"button\" data-t=\"bVal\">Valida<\/button>\n     <button class=\"btn\" id=\"rfflCmd\" type=\"button\" data-t=\"bCmd\">Comando DOS\u2026<\/button>\n     <button class=\"btn\" id=\"rfflSep\" type=\"button\" data-t=\"bSep\">Separatore\u2026<\/button>\n     <span class=\"sep\"><\/span>\n     <button class=\"btn\" id=\"rfflRdD\" type=\"button\" data-t=\"bRdDisk\">Disco \u2192 immagine<\/button>\n     <button class=\"btn\" id=\"rfflWrD\" type=\"button\" data-t=\"bWrDisk\">Immagine \u2192 disco<\/button>\n    <\/div>\n    <div class=\"prog\" id=\"rfflProg\" hidden>\n     <div class=\"top\"><span id=\"rfflProgL\"><\/span><button class=\"btn sm warn\" id=\"rfflStop\" type=\"button\" data-t=\"bStop\">Interrompi<\/button><\/div>\n     <div class=\"bar\" id=\"rfflBar\"><i><\/i><\/div>\n     <div class=\"eta\" id=\"rfflEta\"><\/div>\n    <\/div>\n    <div class=\"pbody\">\n     <div class=\"scr\" id=\"rfflScr\" role=\"list\"><\/div>\n     <p class=\"drophint\" data-t=\"dropDrv\">Trascina qui file PRG o SEQ per scriverli sul disco<\/p>\n    <\/div>\n   <\/section>\n\n   <section class=\"panel\" id=\"rfflPc\">\n    <div class=\"tabs\" role=\"tablist\">\n     <button role=\"tab\" data-tab=\"files\" aria-selected=\"true\"><span data-t=\"tFiles\">File del PC<\/span><span class=\"badge\" id=\"rfflNFiles\">0<\/span><\/button>\n     <button role=\"tab\" data-tab=\"img\" aria-selected=\"false\" data-t=\"tImg\">Immagine disco<\/button>\n     <button role=\"tab\" data-tab=\"csdb\" aria-selected=\"false\">CSDb<\/button>\n     <button role=\"tab\" data-tab=\"log\" aria-selected=\"false\" data-t=\"tLog\">Registro<\/button>\n    <\/div>\n\n    <div class=\"pane\" id=\"rfflPFiles\">\n     <div class=\"drop\" id=\"rfflDrop\">\n      <div class=\"big\" data-t=\"dropBig\">Trascina qui i file dal tuo computer<\/div>\n      <div data-t=\"dropSmall\">PRG, P00, SEQ, immagini D64, D71, D81 e archivi ZIP<\/div>\n      <div class=\"btns\">\n       <button class=\"btn\" id=\"rfflPick\" type=\"button\"><span class=\"i\">\ud83d\udcc4<\/span><span data-t=\"bPick\">Scegli file<\/span><\/button>\n       <button class=\"btn\" id=\"rfflPickDir\" type=\"button\"><span class=\"i\">\ud83d\udcc1<\/span><span data-t=\"bPickDir\">Apri una cartella<\/span><\/button>\n      <\/div>\n      <input type=\"file\" id=\"rfflInp\" multiple hidden accept=\".prg,.p00,.seq,.s00,.usr,.u00,.d64,.d71,.d81,.zip\">\n      <input type=\"file\" id=\"rfflInpDir\" webkitdirectory multiple hidden>\n     <\/div>\n     <div class=\"flist\" id=\"rfflList\"><\/div>\n     <div class=\"listbar\" id=\"rfflListBar\" hidden>\n      <span id=\"rfflListInfo\"><\/span><span class=\"sp\"><\/span>\n      <button class=\"btn sm\" id=\"rfflWrAll\" type=\"button\" data-t=\"bWrAll\">Scrivi tutti sul disco<\/button>\n      <button class=\"btn sm\" id=\"rfflClr\" type=\"button\" data-t=\"bClr\">Svuota elenco<\/button>\n     <\/div>\n    <\/div>\n\n    <div class=\"pane\" id=\"rfflPImg\" hidden><\/div>\n\n    <div class=\"pane\" id=\"rfflPCsdb\" hidden>\n     <form class=\"csform\" id=\"rfflCsF\">\n      <input type=\"search\" id=\"rfflCsQ\" autocomplete=\"off\" spellcheck=\"false\" data-tp=\"csPh\" placeholder=\"\">\n      <button class=\"btn pri\" type=\"submit\" data-t=\"bSearch\">Cerca<\/button>\n     <\/form>\n     <p class=\"csnote\" data-th=\"csNote\"><\/p>\n     <div id=\"rfflCsOut\"><\/div>\n    <\/div>\n\n    <div class=\"pane\" id=\"rfflPLog\" hidden>\n     <div class=\"logbar\">\n      <label class=\"ck\"><input type=\"checkbox\" id=\"rfflLogBus\" checked><span data-t=\"logBus\">mostra i comandi del bus e USB<\/span><\/label>\n      <span class=\"sp\"><\/span>\n      <button class=\"btn sm\" id=\"rfflLogCp\" type=\"button\" data-t=\"bCopy\">Copia<\/button>\n      <button class=\"btn sm\" id=\"rfflLogClr\" type=\"button\" data-t=\"bClear\">Svuota<\/button>\n     <\/div>\n     <div class=\"log\" id=\"rfflLog\" aria-live=\"off\"><\/div>\n    <\/div>\n   <\/section>\n  <\/div>\n\n  <div class=\"credits\" data-th=\"credits\"><\/div>\n <\/div>\n <dialog class=\"dlg\" id=\"rfflDlg\"><\/dialog>\n<\/div>\n<div class=\"rfxg bottom\" data-no-translation>\n <div class=\"wrap\">\n\n  <section class=\"sec\" id=\"cose\">\n   <div data-lang=\"it\">\n    <h3><span class=\"n\">1<\/span>Cos'\u00e8 la XUM1541<\/h3>\n    <p>I drive Commodore parlano con il computer attraverso il <b>bus seriale IEC<\/b>: un cavo con la presa DIN a 6 poli e cinque segnali (ATN, CLK, DATA, RESET, SRQ) con tempi di pochi microsecondi. Un PC non ha niente del genere, e una semplice porta USB non riesce a rispettare quei tempi.<\/p>\n    <p>La XUM1541 risolve il problema con un microcontrollore che ha l'USB integrato: il suo firmware, scritto da Nate Lawson e distribuito con il progetto <a href=\"https:\/\/github.com\/OpenCBM\/OpenCBM\" target=\"_blank\" rel=\"noopener\">OpenCBM<\/a>, gestisce da solo tutti i tempi del bus, mentre il computer gli manda solo comandi ad alto livello (\u201cparla con il drive 8\u201d, \u201cmanda questi byte\u201d, \u201cleggi la risposta\u201d).<\/p>\n    <div class=\"cols\">\n     <div class=\"card\"><h4>XUM1541 fai-da-te<\/h4><p>Un Arduino Pro Micro e una presa DIN: \u00e8 la versione di questa guida, costa pochi euro di componenti.<\/p><\/div>\n     <div class=\"card\"><h4>ZoomFloppy<\/h4><p>La versione commerciale di RETRO Innovations, con porta parallela per i cavi XP1541 e bus IEEE-488 per i drive PET. Usa lo stesso firmware e funziona anche con il nostro programma.<\/p><\/div>\n     <div class=\"card\"><h4>Drive compatibili<\/h4><p>1541, 1541-II, 1570, 1571, 1581 e in generale le periferiche sul bus seriale del C64 e del VIC-20.<\/p><\/div>\n    <\/div>\n   <\/div>\n   <div data-lang=\"en\" hidden>\n    <h3><span class=\"n\">1<\/span>What the XUM1541 is<\/h3>\n    <p>Commodore drives talk to the computer over the <b>IEC serial bus<\/b>: a cable with a 6-pin DIN plug and five signals (ATN, CLK, DATA, RESET, SRQ) timed in microseconds. A PC has nothing like it, and a plain USB port cannot keep those timings.<\/p>\n    <p>The XUM1541 solves this with a microcontroller that has built-in USB: its firmware, written by Nate Lawson and shipped with the <a href=\"https:\/\/github.com\/OpenCBM\/OpenCBM\" target=\"_blank\" rel=\"noopener\">OpenCBM<\/a> project, handles all the bus timing on its own, while the computer only sends high-level commands (\u201ctalk to drive 8\u201d, \u201csend these bytes\u201d, \u201cread the answer\u201d).<\/p>\n    <div class=\"cols\">\n     <div class=\"card\"><h4>DIY XUM1541<\/h4><p>An Arduino Pro Micro and a DIN socket: the version in this guide, a few euros of parts.<\/p><\/div>\n     <div class=\"card\"><h4>ZoomFloppy<\/h4><p>The commercial version by RETRO Innovations, with a parallel port for XP1541 cables and an IEEE-488 bus for PET drives. Same firmware, and it works with our program too.<\/p><\/div>\n     <div class=\"card\"><h4>Compatible drives<\/h4><p>1541, 1541-II, 1570, 1571, 1581 and in general the devices on the C64 and VIC-20 serial bus.<\/p><\/div>\n    <\/div>\n   <\/div>\n  <\/section>\n\n  <section class=\"sec\" id=\"com-e-fatta\">\n   <div data-lang=\"it\">\n    <h3><span class=\"n\">2<\/span>Com'\u00e8 fatta<\/h3>\n    <p>Nella versione pi\u00f9 semplice non ci sono altri componenti: le linee del bus IEC vanno saldate direttamente ai pin del Pro Micro. Il firmware usa i pin in modalit\u00e0 \u201copen collector\u201d (li porta a massa oppure li lascia liberi), proprio come fa il C64.<\/p>\n   <\/div>\n   <div data-lang=\"en\" hidden>\n    <h3><span class=\"n\">2<\/span>How it's made<\/h3>\n    <p>In the simplest version there are no other parts: the IEC bus lines are soldered straight to the Pro Micro pins. The firmware drives the pins \u201copen collector\u201d style (it pulls them to ground or leaves them free), exactly like the C64 does.<\/p>\n   <\/div>\n   <figure>\n    <svg viewBox=\"0 0 960 330\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"PC USB \u2192 Arduino Pro Micro \u2192 DIN 6 \u2192 1541\">\n     <defs><marker id=\"rfxA\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto\"><path d=\"M0 0L10 5L0 10z\" fill=\"#7c70da\"\/><\/marker><\/defs>\n     <g font-family=\"VT323,monospace\" font-size=\"20\">\n      <rect x=\"20\" y=\"105\" width=\"150\" height=\"100\" rx=\"8\" fill=\"#1d1846\" stroke=\"#7c70da\" stroke-width=\"2\"\/>\n      <rect x=\"36\" y=\"120\" width=\"118\" height=\"58\" rx=\"3\" fill=\"#40318d\"\/>\n      <text x=\"95\" y=\"155\" fill=\"#ffd23f\" text-anchor=\"middle\" font-size=\"22\">PC<\/text>\n      <text x=\"95\" y=\"198\" fill=\"#aaa5d8\" text-anchor=\"middle\" font-size=\"16\">Chrome \u00b7 Edge<\/text>\n      <path d=\"M170 155 H262\" stroke=\"#7c70da\" stroke-width=\"5\" fill=\"none\" marker-end=\"url(#rfxA)\"\/>\n      <text x=\"216\" y=\"144\" fill=\"#3fd8ff\" text-anchor=\"middle\">USB<\/text>\n      <rect x=\"270\" y=\"58\" width=\"250\" height=\"214\" rx=\"6\" fill=\"#123a5a\" stroke=\"#3fd8ff\" stroke-width=\"2\"\/>\n      <rect x=\"270\" y=\"140\" width=\"22\" height=\"30\" fill=\"#c8c8d0\"\/>\n      <rect x=\"340\" y=\"120\" width=\"70\" height=\"70\" fill=\"#111\" stroke=\"#555\"\/>\n      <text x=\"375\" y=\"160\" fill=\"#ccc\" text-anchor=\"middle\" font-size=\"15\">32U4<\/text>\n      <text x=\"395\" y=\"90\" fill=\"#eceaff\" text-anchor=\"middle\" font-size=\"19\">ARDUINO PRO MICRO<\/text>\n      <text x=\"375\" y=\"228\" fill=\"#aaa5d8\" text-anchor=\"middle\" font-size=\"16\">5V \u00b7 16 MHz<\/text><text x=\"375\" y=\"250\" fill=\"#aaa5d8\" text-anchor=\"middle\" font-size=\"16\">firmware xum1541<\/text>\n      <g fill=\"#ffd23f\" font-size=\"19\">\n       <text x=\"508\" y=\"114\" text-anchor=\"end\">16<\/text><text x=\"508\" y=\"140\" text-anchor=\"end\">14<\/text><text x=\"508\" y=\"166\" text-anchor=\"end\">8<\/text>\n       <text x=\"508\" y=\"192\" text-anchor=\"end\">9<\/text><text x=\"508\" y=\"218\" text-anchor=\"end\">10<\/text><text x=\"508\" y=\"244\" text-anchor=\"end\">GND<\/text>\n      <\/g>\n      <g stroke-width=\"3\" fill=\"none\">\n       <path d=\"M520 108 H700\" stroke=\"#5ad15f\"\/><path d=\"M520 134 H700\" stroke=\"#3fd8ff\"\/><path d=\"M520 160 H700\" stroke=\"#e0398a\"\/>\n       <path d=\"M520 186 H700\" stroke=\"#aaa5d8\"\/><path d=\"M520 212 H700\" stroke=\"#f94144\"\/><path d=\"M520 238 H700\" stroke=\"#666\"\/>\n      <\/g>\n      <g fill=\"#eceaff\" font-size=\"18\">\n       <text x=\"610\" y=\"102\" text-anchor=\"middle\">DATA<\/text><text x=\"610\" y=\"128\" text-anchor=\"middle\">CLK<\/text><text x=\"610\" y=\"154\" text-anchor=\"middle\">ATN<\/text>\n       <text x=\"610\" y=\"180\" text-anchor=\"middle\">SRQ<\/text><text x=\"610\" y=\"206\" text-anchor=\"middle\">RESET<\/text><text x=\"610\" y=\"232\" text-anchor=\"middle\">GND<\/text>\n      <\/g>\n      <rect x=\"700\" y=\"86\" width=\"86\" height=\"172\" rx=\"6\" fill=\"#1d1846\" stroke=\"#ffd23f\" stroke-width=\"2\"\/>\n      <g fill=\"#ffd23f\" font-size=\"19\">\n       <text x=\"745\" y=\"114\" text-anchor=\"middle\">5<\/text><text x=\"745\" y=\"140\" text-anchor=\"middle\">4<\/text><text x=\"745\" y=\"166\" text-anchor=\"middle\">3<\/text>\n       <text x=\"745\" y=\"192\" text-anchor=\"middle\">1<\/text><text x=\"745\" y=\"218\" text-anchor=\"middle\">6<\/text><text x=\"745\" y=\"244\" text-anchor=\"middle\">2<\/text>\n      <\/g>\n      <text x=\"743\" y=\"76\" fill=\"#aaa5d8\" text-anchor=\"middle\" font-size=\"17\">DIN 6<\/text>\n      <path d=\"M786 172 H842\" stroke=\"#7c70da\" stroke-width=\"5\" fill=\"none\" marker-end=\"url(#rfxA)\"\/>\n      <rect x=\"850\" y=\"112\" width=\"96\" height=\"120\" rx=\"6\" fill=\"#d8d2c0\" stroke=\"#7c70da\" stroke-width=\"2\"\/>\n      <rect x=\"860\" y=\"150\" width=\"76\" height=\"10\" fill=\"#3a3530\"\/>\n      <rect x=\"866\" y=\"200\" width=\"16\" height=\"6\" fill=\"#f94144\"\/>\n      <text x=\"898\" y=\"134\" fill=\"#3a3530\" text-anchor=\"middle\" font-size=\"19\">1541<\/text>\n     <\/g>\n    <\/svg>\n    <figcaption data-lang=\"it\">Schema di principio: a sinistra i pin del Pro Micro (numeri stampati sulla scheda), a destra i pin della presa DIN del bus IEC.<\/figcaption>\n    <figcaption data-lang=\"en\" hidden>Block diagram: on the left the Pro Micro pins (numbers printed on the board), on the right the pins of the IEC DIN socket.<\/figcaption>\n   <\/figure>\n   <div class=\"tbl\"><table>\n    <thead><tr><th data-lang=\"it\">Pro Micro (scritta sulla scheda)<\/th><th data-lang=\"en\" hidden>Pro Micro (board marking)<\/th><th>ATmega32U4<\/th><th data-lang=\"it\">Segnale<\/th><th data-lang=\"en\" hidden>Signal<\/th><th data-lang=\"it\">Pin DIN IEC<\/th><th data-lang=\"en\" hidden>IEC DIN pin<\/th><\/tr><\/thead>\n    <tbody>\n     <tr><td class=\"p\">16<\/td><td>PB2<\/td><td>DATA<\/td><td class=\"p\">5<\/td><\/tr>\n     <tr><td class=\"p\">14<\/td><td>PB3<\/td><td>CLK<\/td><td class=\"p\">4<\/td><\/tr>\n     <tr><td class=\"p\">8<\/td><td>PB4<\/td><td>ATN<\/td><td class=\"p\">3<\/td><\/tr>\n     <tr><td class=\"p\">9<\/td><td>PB5<\/td><td>SRQ<\/td><td class=\"p\">1<\/td><\/tr>\n     <tr><td class=\"p\">10<\/td><td>PB6<\/td><td>RESET<\/td><td class=\"p\">6<\/td><\/tr>\n     <tr><td class=\"p\">GND<\/td><td>\u2014<\/td><td>GND<\/td><td class=\"p\">2<\/td><\/tr>\n    <\/tbody>\n   <\/table><\/div>\n   <div data-lang=\"it\">\n    <div class=\"note warn\"><b>Il 5 V non va collegato<\/b> alla presa IEC: il Pro Micro si alimenta dall'USB e il drive ha il suo alimentatore. Si collega solo la massa (GND).<\/div>\n    <h4>Collegamenti facoltativi<\/h4>\n    <ul>\n     <li><b>Cavo parallelo XP1541<\/b> (per le copie veloci di OpenCBM e per nibtools, con il drive modificato): D0\u2013D7 sui pin 3, 2, RXI, TXO, A3, A2, A1, A0. Nel circuito di tebl \u00e8 il connettore J2 a 2\u00d78 pin.<\/li>\n     <li><b>Versione con 7406<\/b>: un invertitore open collector tra Pro Micro e bus, come nella ZoomFloppy originale; usa altri pin come ingressi (5, 15, 7, 4, 6) e un firmware diverso (<code>PROMICRO_7406<\/code>). Protegge meglio il microcontrollore e permette la lettura \u201cnibbler\u201d sulla 1571.<\/li>\n     <li><b>LED<\/b> con resistenza da 330 \u03a9 per vedere quando la scheda \u00e8 alimentata, e un <b>pulsante di reset<\/b> per riprogrammarla senza toccare i pin con un filo.<\/li>\n    <\/ul>\n   <\/div>\n   <div data-lang=\"en\" hidden>\n    <div class=\"note warn\"><b>Do not connect 5 V<\/b> to the IEC socket: the Pro Micro is powered from USB and the drive has its own power supply. Only ground (GND) is shared.<\/div>\n    <h4>Optional connections<\/h4>\n    <ul>\n     <li><b>XP1541 parallel cable<\/b> (for OpenCBM's fast copies and for nibtools, with a modified drive): D0\u2013D7 on pins 3, 2, RXI, TXO, A3, A2, A1, A0. In tebl's board it is the 2\u00d78 header J2.<\/li>\n     <li><b>7406 version<\/b>: an open-collector inverter between the Pro Micro and the bus, as in the original ZoomFloppy; it uses other pins as inputs (5, 15, 7, 4, 6) and a different firmware (<code>PROMICRO_7406<\/code>). It protects the microcontroller better and allows \u201cnibbler\u201d reads on the 1571.<\/li>\n     <li>An <b>LED<\/b> with a 330 \u03a9 resistor to see when the board is powered, and a <b>reset button<\/b> to reprogram it without touching pins with a wire.<\/li>\n    <\/ul>\n   <\/div>\n  <\/section>\n\n  <section class=\"sec\" id=\"costruirla\">\n   <div data-lang=\"it\">\n    <h3><span class=\"n\">3<\/span>Costruirla<\/h3>\n    <p>Per la nostra abbiamo seguito il progetto <a href=\"https:\/\/github.com\/tebl\/C64-XUM1541\" target=\"_blank\" rel=\"noopener\">C64-XUM1541 di tebl<\/a>: un circuito stampato pensato per non dover saldare fili, con il Pro Micro su zoccolo, la presa DIN e un pulsante. I file Gerber per farlo produrre sono nel suo repository (e su <a href=\"https:\/\/www.pcbway.com\/project\/shareproject\/Commodore_64_XUM1541.html\" target=\"_blank\" rel=\"noopener\">PCBWay<\/a>). Lo stesso schema si pu\u00f2 realizzare anche su millefori o con pochi fili.<\/p>\n    <div class=\"tbl\"><table>\n     <thead><tr><th>Rif.<\/th><th>Componente<\/th><th>Q.t\u00e0<\/th><\/tr><\/thead>\n     <tbody>\n      <tr><td>A1<\/td><td><b>Arduino Pro Micro 5 V \/ 16 MHz<\/b> (ATmega32U4)<\/td><td>1<\/td><\/tr>\n      <tr><td>A1<\/td><td>2 strip da 12 pin maschio, oppure femmina per poter togliere il Pro Micro<\/td><td>2<\/td><\/tr>\n      <tr><td>J1<\/td><td>Presa DIN 6 poli femmina da circuito stampato<\/td><td>1<\/td><\/tr>\n      <tr><td>SW1<\/td><td>Pulsante 6\u00d76 mm ad angolo retto (serve per caricare il firmware)<\/td><td>1<\/td><\/tr>\n      <tr><td>D1, R1<\/td><td>LED 5 mm e resistenza da 330 \u03a9 (facoltativi)<\/td><td>(1)<\/td><\/tr>\n      <tr><td>J2<\/td><td>Connettore 2\u00d78 pin per il cavo parallelo (facoltativo)<\/td><td>(1)<\/td><\/tr>\n      <tr><td>\u2014<\/td><td>Scatola 70\u00d745\u00d730 mm (facoltativa)<\/td><td>(1)<\/td><\/tr>\n      <tr><td>\u2014<\/td><td>Cavo seriale IEC DIN 6 maschio\u2013maschio, come quello tra C64 e 1541<\/td><td>1<\/td><\/tr>\n     <\/tbody>\n    <\/table><\/div>\n    <div class=\"note\"><b>Attenzione al Pro Micro<\/b>: serve la versione a <b>5 V e 16 MHz<\/b> (quella a 3,3 V \/ 8 MHz non va bene). Esistono cloni di dimensioni leggermente diverse: prima di saldare controlla che i pin coincidano con il circuito. Sui cloni il ponticello J1 deve essere chiuso (di solito lo \u00e8 gi\u00e0).<\/div>\n   <\/div>\n   <div data-lang=\"en\" hidden>\n    <h3><span class=\"n\">3<\/span>Building it<\/h3>\n    <p>For ours we followed <a href=\"https:\/\/github.com\/tebl\/C64-XUM1541\" target=\"_blank\" rel=\"noopener\">tebl's C64-XUM1541<\/a> project: a printed circuit board designed so you don't have to solder wires, with the Pro Micro on headers, the DIN socket and a button. The Gerber files to have it made are in his repository (and on <a href=\"https:\/\/www.pcbway.com\/project\/shareproject\/Commodore_64_XUM1541.html\" target=\"_blank\" rel=\"noopener\">PCBWay<\/a>). The same circuit can be built on perfboard or with a few wires.<\/p>\n    <div class=\"tbl\"><table>\n     <thead><tr><th>Ref.<\/th><th>Part<\/th><th>Qty<\/th><\/tr><\/thead>\n     <tbody>\n      <tr><td>A1<\/td><td><b>Arduino Pro Micro 5 V \/ 16 MHz<\/b> (ATmega32U4)<\/td><td>1<\/td><\/tr>\n      <tr><td>A1<\/td><td>2 \u00d7 12-pin male headers, or female ones to be able to remove the Pro Micro<\/td><td>2<\/td><\/tr>\n      <tr><td>J1<\/td><td>6-pin female DIN socket, PCB mount<\/td><td>1<\/td><\/tr>\n      <tr><td>SW1<\/td><td>6\u00d76 mm right-angle push button (used to load the firmware)<\/td><td>1<\/td><\/tr>\n      <tr><td>D1, R1<\/td><td>5 mm LED and 330 \u03a9 resistor (optional)<\/td><td>(1)<\/td><\/tr>\n      <tr><td>J2<\/td><td>2\u00d78-pin header for the parallel cable (optional)<\/td><td>(1)<\/td><\/tr>\n      <tr><td>\u2014<\/td><td>70\u00d745\u00d730 mm project box (optional)<\/td><td>(1)<\/td><\/tr>\n      <tr><td>\u2014<\/td><td>6-pin DIN male\u2013male IEC serial cable, like the one between C64 and 1541<\/td><td>1<\/td><\/tr>\n     <\/tbody>\n    <\/table><\/div>\n    <div class=\"note\"><b>Mind the Pro Micro<\/b>: you need the <b>5 V, 16 MHz<\/b> version (the 3.3 V \/ 8 MHz one will not do). Some clones have slightly different sizes: check that the pins match the board before soldering. On clones the J1 jumper must be closed (it usually is).<\/div>\n   <\/div>\n  <\/section>\n\n  <section class=\"sec\" id=\"firmware\">\n   <div data-lang=\"it\">\n    <h3><span class=\"n\">4<\/span>Caricare il firmware<\/h3>\n    <p>Il firmware ufficiale \u00e8 nel repository di OpenCBM, gi\u00e0 compilato per il Pro Micro:<\/p>\n    <ul>\n     <li><a href=\"https:\/\/github.com\/OpenCBM\/OpenCBM\/raw\/master\/xum1541\/xum1541-PROMICRO-v08.hex\" rel=\"noopener\">xum1541-PROMICRO-v08.hex<\/a>: linee saldate direttamente al Pro Micro (il circuito di tebl);<\/li>\n     <li><a href=\"https:\/\/github.com\/OpenCBM\/OpenCBM\/raw\/master\/xum1541\/xum1541-PROMICRO_7406-v08.hex\" rel=\"noopener\">xum1541-PROMICRO_7406-v08.hex<\/a>: versione con l'integrato 7406.<\/li>\n    <\/ul>\n    <p>La versione 8 \u00e8 quella che abbiamo provato con il programma di questa pagina. Il Pro Micro ha un bootloader (Caterina) che resta dopo la programmazione, quindi il firmware si pu\u00f2 cambiare quando si vuole. Si programma con <b>avrdude<\/b>, che si trova gi\u00e0 installato con l'Arduino IDE.<\/p>\n    <ol class=\"steps\">\n     <li>Collega il Pro Micro al PC con il cavo USB, <b>senza<\/b> drive collegato.<\/li>\n     <li>Premi <b>due volte di seguito<\/b> il pulsante di reset (o tocca due volte il pin RST con GND). Il LED lampeggia piano: per circa <b>8 secondi<\/b> il bootloader compare come una nuova porta COM in Gestione dispositivi. Attenzione: non \u00e8 la stessa porta che vedi prima del reset.<\/li>\n     <li>Entro quegli 8 secondi lancia avrdude con la porta del bootloader (qui <code>COM10<\/code>):<\/li>\n    <\/ol>\n<pre><code>avrdude -p m32u4 -c avr109 -P COM10 -b 57600 -D -U flash:w:xum1541-PROMICRO-v08.hex:i<\/code><\/pre>\n    <p>Con l'Arduino IDE avrdude si trova in <code>%LOCALAPPDATA%\\Arduino15\\packages\\arduino\\tools\\avrdude\\<\/code> (cartella <code>bin<\/code>, con <code>avrdude.conf<\/code> in <code>etc<\/code> da indicare con <code>-C<\/code>). Su Linux:<\/p>\n<pre><code>stty -F \/dev\/ttyACM0 speed 1200      # reset nel bootloader\navrdude -p m32u4 -c avr109 -P \/dev\/ttyACM0 -U flash:w:xum1541-PROMICRO-v08.hex:i<\/code><\/pre>\n    <div class=\"note ok\">Se tutto \u00e8 andato bene, scollegando e ricollegando la scheda non compare pi\u00f9 una porta COM ma un dispositivo <b>xum1541<\/b> (USB <code>16D0:0504<\/code>).<\/div>\n    <p>La guida di tebl consiglia la versione 7 del firmware, perch\u00e9 con la sua configurazione di OpenCBM la 8 non funzionava: se usi OpenCBM e hai problemi, prova la v07 dal <a href=\"https:\/\/github.com\/tebl\/C64-XUM1541\/tree\/master\/software\/firmware\" target=\"_blank\" rel=\"noopener\">suo repository<\/a>. Il programma di questa pagina accetta le versioni 7 e 8.<\/p>\n   <\/div>\n   <div data-lang=\"en\" hidden>\n    <h3><span class=\"n\">4<\/span>Loading the firmware<\/h3>\n    <p>The official firmware is in the OpenCBM repository, already built for the Pro Micro:<\/p>\n    <ul>\n     <li><a href=\"https:\/\/github.com\/OpenCBM\/OpenCBM\/raw\/master\/xum1541\/xum1541-PROMICRO-v08.hex\" rel=\"noopener\">xum1541-PROMICRO-v08.hex<\/a>: lines soldered straight to the Pro Micro (tebl's board);<\/li>\n     <li><a href=\"https:\/\/github.com\/OpenCBM\/OpenCBM\/raw\/master\/xum1541\/xum1541-PROMICRO_7406-v08.hex\" rel=\"noopener\">xum1541-PROMICRO_7406-v08.hex<\/a>: version with the 7406 chip.<\/li>\n    <\/ul>\n    <p>Version 8 is the one we tested with the program on this page. The Pro Micro has a bootloader (Caterina) that stays after programming, so the firmware can be changed whenever you like. It is programmed with <b>avrdude<\/b>, which already comes with the Arduino IDE.<\/p>\n    <ol class=\"steps\">\n     <li>Connect the Pro Micro to the PC with the USB cable, <b>without<\/b> a drive attached.<\/li>\n     <li>Press the reset button <b>twice in a row<\/b> (or touch the RST pin to GND twice). The LED pulses slowly: for about <b>8 seconds<\/b> the bootloader shows up as a new COM port in Device Manager. Note: it is not the same port you see before the reset.<\/li>\n     <li>Within those 8 seconds run avrdude with the bootloader's port (here <code>COM10<\/code>):<\/li>\n    <\/ol>\n<pre><code>avrdude -p m32u4 -c avr109 -P COM10 -b 57600 -D -U flash:w:xum1541-PROMICRO-v08.hex:i<\/code><\/pre>\n    <p>With the Arduino IDE, avrdude is in <code>%LOCALAPPDATA%\\Arduino15\\packages\\arduino\\tools\\avrdude\\<\/code> (folder <code>bin<\/code>, with <code>avrdude.conf<\/code> in <code>etc<\/code> to be passed with <code>-C<\/code>). On Linux:<\/p>\n<pre><code>stty -F \/dev\/ttyACM0 speed 1200      # reset into the bootloader\navrdude -p m32u4 -c avr109 -P \/dev\/ttyACM0 -U flash:w:xum1541-PROMICRO-v08.hex:i<\/code><\/pre>\n    <div class=\"note ok\">If it worked, when you unplug and replug the board you no longer see a COM port but an <b>xum1541<\/b> device (USB <code>16D0:0504<\/code>).<\/div>\n    <p>tebl's guide recommends firmware version 7, because version 8 did not work with his OpenCBM setup: if you use OpenCBM and have trouble, try v07 from <a href=\"https:\/\/github.com\/tebl\/C64-XUM1541\/tree\/master\/software\/firmware\" target=\"_blank\" rel=\"noopener\">his repository<\/a>. The program on this page accepts versions 7 and 8.<\/p>\n   <\/div>\n  <\/section>\n\n  <section class=\"sec\" id=\"driver\">\n   <div data-lang=\"it\">\n    <h3><span class=\"n\">5<\/span>Installare i driver<\/h3>\n    <h4>Windows<\/h4>\n    <p>Windows non ha un driver per la xum1541: si installa con <a href=\"https:\/\/zadig.akeo.ie\/\" target=\"_blank\" rel=\"noopener\">Zadig<\/a>. Quale scegliere dipende dal programma che vuoi usare:<\/p>\n    <div class=\"tbl\"><table>\n     <thead><tr><th>Driver<\/th><th>Programma di questa pagina (Chrome, Edge)<\/th><th>OpenCBM (riga di comando)<\/th><\/tr><\/thead>\n     <tbody>\n      <tr><td><b>WinUSB<\/b><\/td><td>\u2714 necessario<\/td><td>dipende dalla versione: da provare<\/td><\/tr>\n      <tr><td>libusb-win32<\/td><td>\u2718 il browser non la vede<\/td><td>\u2714 quello indicato nella guida di tebl<\/td><\/tr>\n     <\/tbody>\n    <\/table><\/div>\n    <ol class=\"steps\">\n     <li>Collega la xum1541 (con il firmware gi\u00e0 caricato) e apri Zadig.<\/li>\n     <li>Menu <b>Options \u2192 List All Devices<\/b>, poi scegli <b>xum1541<\/b> nell'elenco (controlla che l'USB ID sia <code>16D0 0504<\/code>).<\/li>\n     <li>A destra della freccia scegli <b>WinUSB<\/b> e premi <b>Replace Driver<\/b> (o <i>Install Driver<\/i>).<\/li>\n    <\/ol>\n    <p>Il driver si pu\u00f2 cambiare quando si vuole rifacendo gli stessi passi con Zadig: per esempio WinUSB per usare la pagina e libusb-win32 per tornare a OpenCBM.<\/p>\n    <h4>Linux<\/h4>\n    <p>Non serve un driver, ma un permesso: crea il file <code>\/etc\/udev\/rules.d\/45-xum1541.rules<\/code> con questa riga, poi ricarica le regole e ricollega la scheda.<\/p>\n<pre><code>SUBSYSTEM==\"usb\", ATTR{idVendor}==\"16d0\", ATTR{idProduct}==\"0504\", TAG+=\"uaccess\"<\/code><\/pre>\n<pre><code>sudo udevadm control --reload-rules<\/code><\/pre>\n    <h4>macOS<\/h4>\n    <p>Non serve niente: la xum1541 \u00e8 subito disponibile per il browser.<\/p>\n   <\/div>\n   <div data-lang=\"en\" hidden>\n    <h3><span class=\"n\">5<\/span>Installing the drivers<\/h3>\n    <h4>Windows<\/h4>\n    <p>Windows has no driver for the xum1541: you install one with <a href=\"https:\/\/zadig.akeo.ie\/\" target=\"_blank\" rel=\"noopener\">Zadig<\/a>. Which one depends on the program you want to use:<\/p>\n    <div class=\"tbl\"><table>\n     <thead><tr><th>Driver<\/th><th>Program on this page (Chrome, Edge)<\/th><th>OpenCBM (command line)<\/th><\/tr><\/thead>\n     <tbody>\n      <tr><td><b>WinUSB<\/b><\/td><td>\u2714 required<\/td><td>depends on the version: to be tested<\/td><\/tr>\n      <tr><td>libusb-win32<\/td><td>\u2718 the browser cannot see it<\/td><td>\u2714 the one in tebl's guide<\/td><\/tr>\n     <\/tbody>\n    <\/table><\/div>\n    <ol class=\"steps\">\n     <li>Plug in the xum1541 (with the firmware already loaded) and open Zadig.<\/li>\n     <li>Menu <b>Options \u2192 List All Devices<\/b>, then pick <b>xum1541<\/b> in the list (check that the USB ID is <code>16D0 0504<\/code>).<\/li>\n     <li>On the right of the arrow choose <b>WinUSB<\/b> and press <b>Replace Driver<\/b> (or <i>Install Driver<\/i>).<\/li>\n    <\/ol>\n    <p>You can switch driver at any time by repeating the same steps in Zadig: for example WinUSB to use this page and libusb-win32 to go back to OpenCBM.<\/p>\n    <h4>Linux<\/h4>\n    <p>No driver is needed, only a permission: create the file <code>\/etc\/udev\/rules.d\/45-xum1541.rules<\/code> with this line, then reload the rules and replug the board.<\/p>\n<pre><code>SUBSYSTEM==\"usb\", ATTR{idVendor}==\"16d0\", ATTR{idProduct}==\"0504\", TAG+=\"uaccess\"<\/code><\/pre>\n<pre><code>sudo udevadm control --reload-rules<\/code><\/pre>\n    <h4>macOS<\/h4>\n    <p>Nothing to install: the xum1541 is available to the browser right away.<\/p>\n   <\/div>\n  <\/section>\n\n  <section class=\"sec\" id=\"usarla\">\n   <div data-lang=\"it\">\n    <h3><span class=\"n\">6<\/span>Usarla con il programma<\/h3>\n    <div class=\"note warn\"><b>Collega e scollega i cavi a drive spento.<\/b> Il bus IEC non ama le prese inserite sotto tensione: prima i cavi, poi accendi il drive.<\/div>\n    <ol class=\"steps\">\n     <li>Apri questa pagina con <b>Chrome<\/b> o <b>Edge<\/b> su computer (Firefox e Safari non hanno WebUSB).<\/li>\n     <li>Collega il cavo IEC tra la xum1541 e il drive, poi la xum1541 al PC, e accendi il drive.<\/li>\n     <li>Nel programma scegli <b>\u25cf Drive reale<\/b> e premi <b>Collega la xum1541<\/b>: il browser mostra l'elenco dei dispositivi compatibili, scegli la xum1541. Il permesso resta: le volte successive basta <i>Ricollega l'ultima<\/i>.<\/li>\n     <li>Il pannello diventa verde con la scritta <b>ONLINE<\/b> e il drive risponde con il suo messaggio, per esempio <code>73,CBM DOS V2.6 1541,00,00<\/code>. Inserisci un floppy e premi <b>Directory<\/b>.<\/li>\n    <\/ol>\n    <h4>Cosa puoi fare<\/h4>\n    <ul>\n     <li><b>Dal floppy al PC<\/b>: scarica un singolo file come <code>.prg<\/code>, oppure con <b>Disco \u2192 immagine<\/b> copia tutto il disco in un D64 (o D71, D81).<\/li>\n     <li><b>Dal PC al floppy<\/b>: trascina sul pannello del drive i file PRG, SEQ o P00, oppure apri un D64 nella scheda \u00abImmagine disco\u00bb e scrivi sul floppy un file alla volta o l'immagine intera con <b>Immagine \u2192 disco<\/b>.<\/li>\n     <li><b>Da CSDb<\/b>: cerca una demo o un gioco, apri il file (anche dentro gli ZIP) e mandalo sul drive.<\/li>\n     <li>Formattare, cancellare, rinominare, validare, mandare comandi DOS e aggiungere i classici separatori <code>\"----------------\" DEL<\/code> alla directory.<\/li>\n    <\/ul>\n    <h4>Quanto ci mette<\/h4>\n    <p>Il programma usa il protocollo seriale standard, lo stesso del C64 senza caricatori veloci: qualche centinaio di byte al secondo. Un file da 30 blocchi (circa 7,5 KB) richiede una ventina di secondi; un disco intero letto blocco per blocco richiede circa dieci minuti. I trasferimenti veloci di OpenCBM (<code>d64copy<\/code>, circa un minuto a disco) caricano un programma nella memoria del drive: non sono ancora nel programma web.<\/p>\n    <h4>Se qualcosa non va<\/h4>\n    <ul>\n     <li><b>La xum1541 non compare nell'elenco<\/b>: su Windows manca il driver WinUSB; su Linux la regola udev.<\/li>\n     <li><b>Compare ma non si apre<\/b>: un altro programma la sta usando (OpenCBM, VICE, un'altra scheda del browser), oppure il driver non \u00e8 WinUSB.<\/li>\n     <li><b>99, DRIVER ERROR<\/b> o \u00abnessun drive risponde\u00bb: il drive \u00e8 spento, il cavo IEC non \u00e8 inserito bene o il numero di periferica non \u00e8 8 (cambialo nel menu <i>Numero<\/i>).<\/li>\n     <li>La scheda <b>Registro<\/b> mostra tutti i comandi mandati al drive: copiala e mandacela se ti serve aiuto.<\/li>\n    <\/ul>\n    <p>Senza xum1541 puoi provare tutto con la <b>Simulazione<\/b>: un drive virtuale in memoria che risponde come quello vero, ma non legge e non scrive nessun floppy. Il visualizzatore di immagini D64 e l'estrazione dei file funzionano in qualsiasi browser.<\/p>\n   <\/div>\n   <div data-lang=\"en\" hidden>\n    <h3><span class=\"n\">6<\/span>Using it with the program<\/h3>\n    <div class=\"note warn\"><b>Plug and unplug cables with the drive switched off.<\/b> The IEC bus does not like hot-plugging: cables first, then switch the drive on.<\/div>\n    <ol class=\"steps\">\n     <li>Open this page in <b>Chrome<\/b> or <b>Edge<\/b> on a computer (Firefox and Safari have no WebUSB).<\/li>\n     <li>Connect the IEC cable between the xum1541 and the drive, then the xum1541 to the PC, and switch the drive on.<\/li>\n     <li>In the program choose <b>\u25cf Real drive<\/b> and press <b>Connect the xum1541<\/b>: the browser lists the compatible devices, pick the xum1541. The permission is kept: next time <i>Reconnect last one<\/i> is enough.<\/li>\n     <li>The panel turns green with <b>ONLINE<\/b> and the drive answers with its message, for example <code>73,CBM DOS V2.6 1541,00,00<\/code>. Insert a floppy and press <b>Directory<\/b>.<\/li>\n    <\/ol>\n    <h4>What you can do<\/h4>\n    <ul>\n     <li><b>From floppy to PC<\/b>: download a single file as <code>.prg<\/code>, or copy the whole disk to a D64 (or D71, D81) with <b>Disk \u2192 image<\/b>.<\/li>\n     <li><b>From PC to floppy<\/b>: drop PRG, SEQ or P00 files on the drive panel, or open a D64 in the \u201cDisk image\u201d tab and write one file at a time or the whole image with <b>Image \u2192 disk<\/b>.<\/li>\n     <li><b>From CSDb<\/b>: search for a demo or a game, open the file (even inside ZIPs) and send it to the drive.<\/li>\n     <li>Format, scratch, rename, validate, send DOS commands and add the classic <code>\"----------------\" DEL<\/code> separators to the directory.<\/li>\n    <\/ul>\n    <h4>How long it takes<\/h4>\n    <p>The program uses the standard serial protocol, the same as a C64 without fast loaders: a few hundred bytes per second. A 30-block file takes about twenty seconds; a whole disk read block by block takes about ten minutes. OpenCBM's fast transfers (<code>d64copy<\/code>, about a minute per disk) load a program into the drive's memory: they are not in the web program yet.<\/p>\n    <h4>If something goes wrong<\/h4>\n    <ul>\n     <li><b>The xum1541 is not in the list<\/b>: on Windows the WinUSB driver is missing; on Linux the udev rule.<\/li>\n     <li><b>It is listed but will not open<\/b>: another program is using it (OpenCBM, VICE, another browser tab), or the driver is not WinUSB.<\/li>\n     <li><b>99, DRIVER ERROR<\/b> or \u201cno drive answers\u201d: the drive is off, the IEC cable is not seated properly or the device number is not 8 (change it in the <i>Device<\/i> menu).<\/li>\n     <li>The <b>Log<\/b> tab shows every command sent to the drive: copy it and send it to us if you need help.<\/li>\n    <\/ul>\n    <p>Without an xum1541 you can try everything with the <b>Simulation<\/b>: a virtual drive in memory that answers like the real one but reads and writes no floppy. The D64 image viewer and file extraction work in any browser.<\/p>\n   <\/div>\n  <\/section>\n\n  <section class=\"sec\" id=\"opencbm\">\n   <div data-lang=\"it\">\n    <h3><span class=\"n\">7<\/span>Con OpenCBM (riga di comando)<\/h3>\n    <p>La stessa scheda funziona con <a href=\"https:\/\/spiro.trikaliotis.net\/opencbm\" target=\"_blank\" rel=\"noopener\">OpenCBM<\/a>, la raccolta di programmi da riga di comando da cui viene il firmware. Su Windows si scompatta lo ZIP, si apre un prompt dei comandi come amministratore nella cartella e si installa con <code>install xum1541<\/code>. Poi:<\/p>\n<pre><code>cbmctrl detect                    rem elenca i drive collegati\ncbmctrl dir 8                     rem directory del disco nel drive 8\nd64copy 8 disco.d64               rem copia veloce del disco in un'immagine\nd64copy -n 8 disco.d64 &gt; disco.log 2&gt;&amp;1   rem idem, con un registro degli errori<\/code><\/pre>\n    <p>Per archiviare una collezione di dischi \u00e8 utile tenere il registro di ogni copia: un disco con errori di lettura si riconosce subito anche dopo anni.<\/p>\n   <\/div>\n   <div data-lang=\"en\" hidden>\n    <h3><span class=\"n\">7<\/span>With OpenCBM (command line)<\/h3>\n    <p>The same board works with <a href=\"https:\/\/spiro.trikaliotis.net\/opencbm\" target=\"_blank\" rel=\"noopener\">OpenCBM<\/a>, the command-line suite the firmware comes from. On Windows, unzip it, open a command prompt as administrator in the folder and install it with <code>install xum1541<\/code>. Then:<\/p>\n<pre><code>cbmctrl detect                    rem list the connected drives\ncbmctrl dir 8                     rem directory of the disk in drive 8\nd64copy 8 disk.d64                rem fast copy of the disk to an image\nd64copy -n 8 disk.d64 &gt; disk.log 2&gt;&amp;1     rem same, with an error log<\/code><\/pre>\n    <p>When archiving a disk collection it helps to keep a log of every copy: a disk with read errors is easy to spot even years later.<\/p>\n   <\/div>\n  <\/section>\n\n  <div class=\"credits\">\n   <p data-lang=\"it\">Fonti e ringraziamenti: firmware xum1541 di Nate Lawson e progetto <a href=\"https:\/\/github.com\/OpenCBM\/OpenCBM\" target=\"_blank\" rel=\"noopener\">OpenCBM<\/a>; circuito stampato e guida di <a href=\"https:\/\/github.com\/tebl\/C64-XUM1541\" target=\"_blank\" rel=\"noopener\">tebl<\/a>, a sua volta basato sull'articolo di <a href=\"https:\/\/myoldcomputer.nl\/commodore-64\/xum1541-promicro\/\" target=\"_blank\" rel=\"noopener\">myoldcomputer.nl<\/a>. Il programma web \u00e8 un'implementazione indipendente di RetroFixer.<\/p>\n   <p data-lang=\"en\" hidden>Sources and thanks: xum1541 firmware by Nate Lawson and the <a href=\"https:\/\/github.com\/OpenCBM\/OpenCBM\" target=\"_blank\" rel=\"noopener\">OpenCBM<\/a> project; PCB and guide by <a href=\"https:\/\/github.com\/tebl\/C64-XUM1541\" target=\"_blank\" rel=\"noopener\">tebl<\/a>, itself based on the <a href=\"https:\/\/myoldcomputer.nl\/commodore-64\/xum1541-promicro\/\" target=\"_blank\" rel=\"noopener\">myoldcomputer.nl<\/a> article. The web program is an independent implementation by RetroFixer.<\/p>\n  <\/div>\n <\/div>\n<\/div>\n\n<script>\n\/* RetroFixer \u00b7 pagina XUM1541: testi della guida in italiano o inglese secondo la lingua della pagina *\/\n(function () {\n  'use strict';\n  var en = \/^en\/i.test(document.documentElement.lang || '');\n  document.querySelectorAll('.rfxg [data-lang]').forEach(function (e) { e.hidden = (e.getAttribute('data-lang') === 'en') !== en; });\n})();\n\n\/* RetroFixer \u00b7 Floppy C64 \u00b7 immagini disco D64 \/ D71 \/ D81\n * Lettura e scrittura di immagini in memoria (Uint8Array): directory, BAM, file, formattazione,\n * cancellazione, rinomina, validazione, blocchi. Usata dal visualizzatore e dal drive simulato.\n *\/\n(function (RF) {\n'use strict';\n\n\/* ---------- PETSCII ---------- *\/\n\/\/ visualizzazione nel set maiuscole\/grafica (quello di accensione del C64)\nvar GFX = {0x5C:'\u00a3',0x5E:'\u2191',0x5F:'\u2190',0x60:'\u2500',0x61:'\u2660',0x62:'\u2502',0x63:'\u2500',0x6B:'\u256e',0x6C:'\u2570',0x6D:'\u256f',0x6E:'\u2572',0x6F:'\u2571',\n  0x71:'\u25cf',0x73:'\u2665',0x75:'\u256d',0x76:'\u2573',0x77:'\u25cb',0x78:'\u2663',0x7A:'\u2666',0x7B:'\u253c',0x7D:'\u2502',0x7E:'\u03c0',0x7F:'\u25e5',0xA0:' ',0xFF:'\u03c0'};\nfunction petChar(b) {\n  if (b >= 0x20 && b <= 0x5B) return String.fromCharCode(b);\n  if (b === 0x5D) return ']';\n  if (GFX[b]) return GFX[b];\n  if (b >= 0xC0 && b <= 0xDF && GFX[b - 0x60]) return GFX[b - 0x60];\n  if (b >= 0xC1 && b <= 0xDA) return String.fromCharCode(b - 0x80); \/\/ lettere \"shiftate\": nel set grafico sono simboli, le mostriamo come lettere\n  if (b >= 0xE0 && b <= 0xFE) return '\u2592';\n  if (b >= 0xA1 && b <= 0xBF) return '\u2592';\n  return '\u00b7';\n}\nfunction petToStr(bytes) { var s = ''; for (var i = 0; i < bytes.length; i++) s += petChar(bytes[i]); return s; }\n\/\/ testo del PC \u2192 PETSCII (maiuscolo, caratteri sconosciuti \u2192 '?')\nfunction strToPet(str) {\n  var out = [], s = String(str).normalize('NFD').replace(\/[\u0300-\u036f]\/g, '').toUpperCase();\n  for (var i = 0; i < s.length; i++) {\n    var c = s.charCodeAt(i);\n    if (c === 0xA3) out.push(0x5C);\n    else if (c >= 0x20 && c <= 0x5D) out.push(c);\n    else out.push(0x3F);\n  }\n  return out;\n}\n\/\/ nome di file del PC \u2192 nome C64 (16 caratteri, senza caratteri vietati nei nomi DOS)\nfunction pcNameToC64(name) {\n  var base = String(name).replace(\/\\.(prg|seq|usr|p00|s00|u00)$\/i, '').replace(\/[_]+\/g, ' ');\n  var p = strToPet(base).filter(function (b) { return b !== 0x2A && b !== 0x3F && b !== 0x2C && b !== 0x3A && b !== 0x3D && b !== 0x22 && b !== 0x24 && b !== 0x40 || false; });\n  while (p.length && p[0] === 0x20) p.shift();\n  return p.slice(0, 16).length ? p.slice(0, 16) : strToPet('SENZA NOME');\n}\nfunction c64NameToPc(bytes, ext) {\n  var s = '';\n  for (var i = 0; i < bytes.length; i++) {\n    var b = bytes[i];\n    if (b >= 0x41 && b <= 0x5A) s += String.fromCharCode(b + 32);\n    else if (b >= 0xC1 && b <= 0xDA) s += String.fromCharCode(b - 0x80);\n    else if ((b >= 0x30 && b <= 0x39) || b === 0x2D || b === 0x2E || b === 0x2B || b === 0x21 || b === 0x28 || b === 0x29) s += String.fromCharCode(b);\n    else if (b === 0x20 || b === 0xA0) s += ' ';\n    else s += '_';\n  }\n  s = s.trim().replace(\/\\s+\/g, ' ') || 'file';\n  return ext ? s + '.' + ext : s;\n}\nfunction bytesEq(a, b) { if (a.length !== b.length) return false; for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; }\n\/\/ confronto con i caratteri jolly del DOS: * = il resto, ? = un carattere qualsiasi\nfunction wildMatch(pat, name) {\n  for (var i = 0; i < pat.length; i++) {\n    if (pat[i] === 0x2A) return true;\n    if (i >= name.length) return false;\n    if (pat[i] !== 0x3F && pat[i] !== name[i]) return false;\n  }\n  return pat.length === name.length;\n}\n\n\/* ---------- formati ---------- *\/\nvar TYPES = ['DEL', 'SEQ', 'PRG', 'USR', 'REL', 'CBM'];\nfunction zone(t) { return t <= 17 ? 21 : t <= 24 ? 19 : t <= 30 ? 18 : 17; }\nvar FORMATS = {\n  d64: {id:'d64', label:'D64', drive:'1541', tracks:35, maxTracks:40, spt:function (t) { return zone(t); },\n        dirTrack:18, header:[18,0], dirStart:[18,1], dos:'2A', fileIl:10, dirIl:3, reserved:[18]},\n  d71: {id:'d71', label:'D71', drive:'1571', tracks:70, maxTracks:70, spt:function (t) { return zone(t > 35 ? t - 35 : t); },\n        dirTrack:18, header:[18,0], dirStart:[18,1], dos:'2A', fileIl:6, dirIl:3, reserved:[18,53]},\n  d81: {id:'d81', label:'D81', drive:'1581', tracks:80, maxTracks:80, spt:function () { return 40; },\n        dirTrack:40, header:[40,0], dirStart:[40,3], dos:'3D', fileIl:1, dirIl:1, reserved:[40]}\n};\nfunction sizeFor(f, tracks) { var n = 0; for (var t = 1; t <= tracks; t++) n += f.spt(t); return n; }\n\/\/ riconosce il formato dalla dimensione (con o senza i byte di errore in coda)\nfunction detect(len) {\n  var c = [['d64',35],['d64',40],['d64',42],['d71',70],['d81',80]];\n  for (var i = 0; i < c.length; i++) {\n    var f = FORMATS[c[i][0]], n = sizeFor(f, c[i][1]);\n    if (len === n * 256 || len === n * 257) return {fmt:f, tracks:c[i][1], sectors:n, errors: len === n * 257};\n  }\n  return null;\n}\n\nfunction DiskError(code, msg, t, s) { var e = new Error(msg); e.code = code; e.t = t || 0; e.s = s || 0; return e; }\n\n\/* ---------- immagine ---------- *\/\nfunction Disk(data, name) {\n  var d = detect(data.length);\n  if (!d) throw DiskError(0, 'Dimensione non riconosciuta (' + data.length + ' byte): servono immagini D64, D71 o D81.');\n  this.fmt = d.fmt; this.tracks = d.tracks; this.sectors = d.sectors;\n  this.data = data.slice(0, d.sectors * 256);\n  this.errorBytes = d.errors ? data.slice(d.sectors * 256) : null;\n  this.name = name || '';\n  this.offs = [0, 0];\n  for (var t = 1; t <= this.tracks; t++) this.offs[t + 1] = this.offs[t] + this.fmt.spt(t) * 256;\n}\nDisk.FORMATS = FORMATS;\nDisk.detect = detect;\nDisk.blank = function (fmtId, nameBytes, idBytes) {\n  var f = FORMATS[fmtId] || FORMATS.d64, d = new Disk(new Uint8Array(sizeFor(f, f.tracks) * 256));\n  d.format(nameBytes, idBytes || strToPet('00'));\n  return d;\n};\n\nvar P = Disk.prototype;\nP.spt = function (t) { return this.fmt.spt(t); };\nP.valid = function (t, s) { return t >= 1 && t <= this.tracks && s >= 0 && s < this.fmt.spt(t); };\nP.off = function (t, s) {\n  if (!this.valid(t, s)) throw DiskError(66, 'ILLEGAL TRACK OR SECTOR', t, s);\n  return this.offs[t] + s * 256;\n};\nP.block = function (t, s) { var o = this.off(t, s); return this.data.subarray(o, o + 256); };\nP.readBlock = function (t, s) { return this.block(t, s).slice(); };\nP.writeBlock = function (t, s, bytes) { this.block(t, s).set(bytes.subarray ? bytes.subarray(0, 256) : bytes.slice(0, 256)); };\nP.sectorIndex = function (t, s) { return this.off(t, s) \/ 256; };\n\/\/ tracce che il DOS usa (le tracce 36-40 di un D64 da 40 tracce non hanno BAM standard)\nP.dosTracks = function () { return this.fmt.id === 'd64' ? 35 : this.tracks; };\n\n\/* ---------- BAM ---------- *\/\nP.bamEntry = function (t) {\n  var f = this.fmt.id;\n  if (f === 'd81') {\n    var sec = t <= 40 ? this.off(40, 1) : this.off(40, 2), tt = t <= 40 ? t : t - 40, e = sec + 0x10 + 6 * (tt - 1);\n    return {count: e, map: e + 1};\n  }\n  if (t <= 35) { var b = this.off(18, 0) + 4 + 4 * (t - 1); return {count: b, map: b + 1}; }\n  if (f === 'd71' && t <= 70) return {count: this.off(18, 0) + 0xDD + (t - 36), map: this.off(53, 0) + 3 * (t - 36)};\n  return null;\n};\nP.isFree = function (t, s) {\n  var e = this.bamEntry(t);\n  if (!e) return false;\n  return (this.data[e.map + (s >> 3)] & (1 << (s & 7))) !== 0;\n};\nP.setFree = function (t, s, free) {\n  var e = this.bamEntry(t);\n  if (!e) return;\n  var i = e.map + (s >> 3), m = 1 << (s & 7), was = (this.data[i] & m) !== 0;\n  if (was === free) return;\n  if (free) { this.data[i] |= m; this.data[e.count]++; }\n  else { this.data[i] &= ~m; this.data[e.count]--; }\n};\nP.trackFree = function (t) { var e = this.bamEntry(t); return e ? this.data[e.count] : 0; };\nP.blocksFree = function () {\n  var n = 0, max = this.dosTracks();\n  for (var t = 1; t <= max; t++) if (this.fmt.reserved.indexOf(t) < 0) n += this.trackFree(t);\n  return n;\n};\nP.blocksTotal = function () {\n  var n = 0, max = this.dosTracks();\n  for (var t = 1; t <= max; t++) if (this.fmt.reserved.indexOf(t) < 0) n += this.fmt.spt(t);\n  return n;\n};\n\/\/ mappa per il disegno: 0 libero, 1 occupato, 2 sistema (traccia directory), 3 fuori dalla BAM\nP.bamMap = function () {\n  var out = [];\n  for (var t = 1; t <= this.tracks; t++) {\n    var row = [];\n    for (var s = 0; s < this.fmt.spt(t); s++) {\n      if (t > this.dosTracks()) row.push(3);\n      else if (this.fmt.reserved.indexOf(t) >= 0) row.push(this.isFree(t, s) ? 0 : 2);\n      else row.push(this.isFree(t, s) ? 0 : 1);\n    }\n    out.push(row);\n  }\n  return out;\n};\n\n\/* ---------- intestazione ---------- *\/\nP.headerInfo = function () {\n  var b, nameOff, idOff;\n  if (this.fmt.id === 'd81') { b = this.block(40, 0); nameOff = 0x04; idOff = 0x16; }\n  else { b = this.block(18, 0); nameOff = 0x90; idOff = 0xA2; }\n  var name = Array.prototype.slice.call(b, nameOff, nameOff + 16);\n  while (name.length && name[name.length - 1] === 0xA0) name.pop();\n  return {name: name, id: [b[idOff], b[idOff + 1]], dos: [b[idOff + 3], b[idOff + 4]], raw: Array.prototype.slice.call(b, nameOff, nameOff + 27)};\n};\n\n\/* ---------- formattazione ---------- *\/\nfunction pad16(bytes) { bytes = bytes || []; var a = new Array(16).fill(0xA0); for (var i = 0; i < 16 && i < bytes.length; i++) a[i] = bytes[i]; return a; }\nP.format = function (nameBytes, idBytes) {\n  var f = this.fmt, id = idBytes && idBytes.length ? [idBytes[0], idBytes.length > 1 ? idBytes[1] : 0x20] : this.headerInfo().id;\n  this.data.fill(0);\n  var t, s, b;\n  if (f.id === 'd81') {\n    b = this.block(40, 0);\n    b.set([40, 3, 0x44, 0]); b.set(pad16(nameBytes), 4); b[20] = b[21] = 0xA0; b[22] = id[0]; b[23] = id[1]; b[24] = 0xA0;\n    b[25] = 0x33; b[26] = 0x44; b[27] = b[28] = 0xA0;\n    [[40, 1, 40, 2], [40, 2, 0, 0xFF]].forEach(function (x) {\n      var bb = this.block(x[0], x[1]);\n      bb.set([x[2], x[3], 0x44, 0xBB, id[0], id[1], 0xC0, 0]);\n    }, this);\n    for (t = 1; t <= 80; t++) for (s = 0; s < 40; s++) this.setFree(t, s, true);\n    for (s = 0; s < 4; s++) this.setFree(40, s, false);\n    b = this.block(40, 3); b[1] = 0xFF;\n  } else {\n    b = this.block(18, 0);\n    b.set([18, 1, 0x41, f.id === 'd71' ? 0x80 : 0]);\n    b.set(pad16(nameBytes), 0x90); b[0xA0] = b[0xA1] = 0xA0; b[0xA2] = id[0]; b[0xA3] = id[1]; b[0xA4] = 0xA0;\n    b[0xA5] = 0x32; b[0xA6] = 0x41; for (var i = 0xA7; i <= 0xAA; i++) b[i] = 0xA0;\n    var last = f.id === 'd71' ? 70 : 35;\n    for (t = 1; t <= last; t++) for (s = 0; s < f.spt(t); s++) this.setFree(t, s, true);\n    this.setFree(18, 0, false); this.setFree(18, 1, false);\n    if (f.id === 'd71') for (s = 0; s < f.spt(53); s++) this.setFree(53, s, false);\n    b = this.block(18, 1); b[1] = 0xFF;\n  }\n  if (this.errorBytes) this.errorBytes.fill(1);\n};\n\/\/ formattazione rapida (N:nome senza ID): azzera directory e BAM, tiene l'ID\nP.quickFormat = function (nameBytes) { this.format(nameBytes, this.headerInfo().id); };\n\n\/* ---------- directory ---------- *\/\nP.dirSectors = function () {\n  var out = [], seen = {}, t = this.fmt.dirStart[0], s = this.fmt.dirStart[1];\n  while (t !== 0 && this.valid(t, s) && !seen[t + ':' + s] && out.length < 200) {\n    seen[t + ':' + s] = 1; out.push([t, s]);\n    var b = this.block(t, s); t = b[0]; s = b[1];\n  }\n  return out;\n};\nP.entries = function (withEmpty) {\n  var out = [], secs = this.dirSectors();\n  for (var k = 0; k < secs.length; k++) {\n    var b = this.block(secs[k][0], secs[k][1]);\n    for (var i = 0; i < 8; i++) {\n      var o = i * 32, type = b[o + 2];\n      if (type === 0 && !withEmpty) continue;\n      var nm = Array.prototype.slice.call(b, o + 5, o + 21);\n      while (nm.length && nm[nm.length - 1] === 0xA0) nm.pop();\n      out.push({dt: secs[k][0], ds: secs[k][1], di: i, rawType: type, type: type & 7, typeName: TYPES[type & 7] || '???',\n        closed: (type & 0x80) !== 0, locked: (type & 0x40) !== 0, t: b[o + 3], s: b[o + 4], name: nm,\n        blocks: b[o + 30] | (b[o + 31] << 8), sideT: b[o + 21], sideS: b[o + 22], recLen: b[o + 23], empty: type === 0});\n    }\n  }\n  return out;\n};\nP.files = function () { return this.entries(false); };\nP.find = function (nameBytes, typeFilter) {\n  var e = this.files();\n  for (var i = 0; i < e.length; i++) {\n    if (!e[i].closed && !typeFilter) continue;\n    if (typeFilter && e[i].type !== typeFilter) continue;\n    if (wildMatch(nameBytes, e[i].name)) return e[i];\n  }\n  return null;\n};\nP.findExact = function (nameBytes) {\n  var e = this.files();\n  for (var i = 0; i < e.length; i++) if (bytesEq(e[i].name, nameBytes)) return e[i];\n  return null;\n};\nP.entryOffset = function (e) { return this.off(e.dt, e.ds) + e.di * 32; };\n\n\/* ---------- lettura dei file ---------- *\/\nP.chain = function (t, s) {\n  var out = [], seen = {};\n  while (t !== 0) {\n    if (!this.valid(t, s)) throw DiskError(66, 'ILLEGAL TRACK OR SECTOR', t, s);\n    if (seen[t + ':' + s]) throw DiskError(66, 'Catena di blocchi circolare', t, s);\n    seen[t + ':' + s] = 1; out.push([t, s]);\n    var b = this.block(t, s); t = b[0]; s = b[1];\n    if (out.length > this.sectors) break;\n  }\n  return out;\n};\nP.readChain = function (t, s) {\n  var c = this.chain(t, s), parts = [], len = 0;\n  for (var i = 0; i < c.length; i++) {\n    var b = this.block(c[i][0], c[i][1]), end = b[0] === 0 ? Math.max(1, b[1]) : 255;\n    var part = b.subarray(2, end + 1); parts.push(part); len += part.length;\n  }\n  var out = new Uint8Array(len), p = 0;\n  parts.forEach(function (x) { out.set(x, p); p += x.length; });\n  return out;\n};\nP.readFile = function (e) { return e.t ? this.readChain(e.t, e.s) : new Uint8Array(0); };\n\n\/* ---------- allocazione ---------- *\/\nP.isReserved = function (t) { return this.fmt.reserved.indexOf(t) >= 0; };\nP.trackOrder = function (startT) {\n  var dt = this.fmt.dirTrack, max = this.dosTracks(), out = [], t;\n  if (this.fmt.id === 'd71') {\n    \/\/ la 1571 riempie prima il lato 1 e poi il lato 2, partendo sempre vicino alla traccia 18\n    for (var d = 1; d < 35; d++) { if (dt - d >= 1) out.push(dt - d); if (dt + d <= 35) out.push(dt + d); }\n    for (t = 36; t <= 70; t++) if (t !== 53) out.push(t);\n    return out;\n  }\n  if (!startT) {\n    for (var k = 1; k < max; k++) { if (dt - k >= 1) out.push(dt - k); if (dt + k <= max) out.push(dt + k); }\n    return out;\n  }\n  var dir = startT < dt ? -1 : 1;\n  for (t = startT; t >= 1 && t <= max; t += dir) if (!this.isReserved(t)) out.push(t);\n  for (t = dt - dir; t >= 1 && t <= max; t -= dir) if (!this.isReserved(t)) out.push(t);\n  return out;\n};\nP.allocate = function (n) {\n  if (this.blocksFree() < n) throw DiskError(72, 'DISK FULL');\n  var out = [], order = this.trackOrder(), cur = null, il = this.fmt.fileIl;\n  for (var i = 0; i < n; i++) {\n    var got = null, tracks = cur ? this.trackOrder(cur[0]) : order;\n    for (var k = 0; k < tracks.length && !got; k++) {\n      var t = tracks[k], spt = this.fmt.spt(t);\n      if (this.isReserved(t) || this.trackFree(t) === 0) continue;\n      var start = cur && cur[0] === t ? (cur[1] + il) % spt : (cur ? (cur[1] + il) % spt : 0);\n      for (var j = 0; j < spt; j++) {\n        var s = (start + j) % spt;\n        if (this.isFree(t, s)) { got = [t, s]; break; }\n      }\n    }\n    if (!got) throw DiskError(72, 'DISK FULL');\n    this.setFree(got[0], got[1], false); out.push(got); cur = got;\n  }\n  return out;\n};\n\/\/ nuovo blocco di directory sulla traccia della directory\nP.newDirSector = function (lastT, lastS) {\n  var t = this.fmt.dirTrack, spt = this.fmt.spt(t);\n  for (var j = 1; j <= spt; j++) {\n    var s = (lastS + this.fmt.dirIl * j) % spt;\n    if (this.isFree(t, s)) {\n      this.setFree(t, s, false);\n      var nb = this.block(t, s); nb.fill(0); nb[1] = 0xFF;\n      var lb = this.block(lastT, lastS); lb[0] = t; lb[1] = s;\n      return [t, s];\n    }\n  }\n  throw DiskError(72, 'DISK FULL');\n};\nP.freeSlot = function () {\n  var e = this.entries(true);\n  for (var i = 0; i < e.length; i++) if (e[i].empty) return e[i];\n  var secs = this.dirSectors(), last = secs[secs.length - 1], ns = this.newDirSector(last[0], last[1]);\n  return {dt: ns[0], ds: ns[1], di: 0};\n};\n\n\/* ---------- scrittura ---------- *\/\nP.writeFile = function (nameBytes, data, type, opts) {\n  opts = opts || {};\n  type = type || 2;\n  if (!nameBytes.length) throw DiskError(34, 'SYNTAX ERROR');\n  for (var q = 0; q < nameBytes.length; q++) if (nameBytes[q] === 0x2A || nameBytes[q] === 0x3F) throw DiskError(33, 'SYNTAX ERROR');\n  var old = this.findExact(nameBytes);\n  if (old) { if (opts.replace) this.scratchEntry(old); else throw DiskError(63, 'FILE EXISTS'); }\n  var nBlocks = Math.max(1, Math.ceil(data.length \/ 254));\n  if (this.blocksFree() < nBlocks) throw DiskError(72, 'DISK FULL');\n  var slot = this.freeSlot();\n  var secs = this.allocate(nBlocks);\n  for (var i = 0; i < secs.length; i++) {\n    var b = this.block(secs[i][0], secs[i][1]), part = data.subarray(i * 254, i * 254 + 254);\n    b.fill(0);\n    if (i < secs.length - 1) { b[0] = secs[i + 1][0]; b[1] = secs[i + 1][1]; }\n    else { b[0] = 0; b[1] = part.length + 1; }\n    b.set(part, 2);\n  }\n  var o = this.off(slot.dt, slot.ds) + slot.di * 32, blk = this.data;\n  for (var k = 2; k < 32; k++) blk[o + k] = 0;\n  blk[o + 2] = 0x80 | type; blk[o + 3] = secs[0][0]; blk[o + 4] = secs[0][1];\n  blk.set(pad16(nameBytes), o + 5);\n  blk[o + 30] = nBlocks & 255; blk[o + 31] = nBlocks >> 8;\n  return nBlocks;\n};\n\n\/\/ separatore: voce DEL da 0 blocchi che punta al primo blocco della directory (vedi Drive.addSeparator)\nP.addSeparator = function (nameBytes, locked) {\n  var slot = this.freeSlot(), o = this.off(slot.dt, slot.ds) + slot.di * 32, b = this.data;\n  for (var k = 2; k < 32; k++) b[o + k] = 0;\n  b[o + 2] = locked ? 0xC0 : 0x80; b[o + 3] = this.fmt.dirStart[0]; b[o + 4] = this.fmt.dirStart[1];\n  b.set(pad16(nameBytes), o + 5);\n};\nP.removeEntry = function (e) { this.data[this.entryOffset(e) + 2] = 0; };\n\n\/* ---------- cancellazione, rinomina, validazione ---------- *\/\nP.freeChainSafe = function (t, s) {\n  var seen = {}, n = 0;\n  while (t !== 0 && this.valid(t, s) && !seen[t + ':' + s] && n <= this.sectors) {\n    seen[t + ':' + s] = 1; n++;\n    this.setFree(t, s, true);\n    var b = this.block(t, s); t = b[0]; s = b[1];\n  }\n};\nP.scratchEntry = function (e) {\n  if (e.t) this.freeChainSafe(e.t, e.s);\n  if (e.type === 4 && e.sideT) this.freeChainSafe(e.sideT, e.sideS);\n  this.data[this.entryOffset(e) + 2] = 0;\n};\n\/\/ S:modello1,modello2 \u2192 numero di file cancellati (i file protetti con \"<\" restano)\nP.scratch = function (patterns) {\n  var n = 0, self = this;\n  patterns.forEach(function (pat) {\n    self.files().forEach(function (e) {\n      if (!e.locked && wildMatch(pat, e.name)) { self.scratchEntry(e); n++; }\n    });\n  });\n  return n;\n};\nP.rename = function (newName, oldName) {\n  if (!newName.length || !oldName.length) throw DiskError(34, 'SYNTAX ERROR');\n  if (this.findExact(newName)) throw DiskError(63, 'FILE EXISTS');\n  var e = this.find(oldName, 0) || null;\n  if (!e) { var all = this.files(); for (var i = 0; i < all.length; i++) if (wildMatch(oldName, all[i].name)) { e = all[i]; break; } }\n  if (!e) throw DiskError(62, 'FILE NOT FOUND');\n  this.data.set(pad16(newName), this.entryOffset(e) + 5);\n};\n\/\/ V: ricostruisce la BAM dalla directory e toglie i file non chiusi (\"*PRG\")\nP.validate = function () {\n  var self = this, max = this.dosTracks(), t, s, removed = 0;\n  for (t = 1; t <= max; t++) for (s = 0; s < this.fmt.spt(t); s++) this.setFree(t, s, true);\n  if (this.fmt.id === 'd81') { for (s = 0; s < 3; s++) this.setFree(40, s, false); }\n  else { this.setFree(18, 0, false); if (this.fmt.id === 'd71') for (s = 0; s < this.fmt.spt(53); s++) this.setFree(53, s, false); }\n  this.dirSectors().forEach(function (x) { self.setFree(x[0], x[1], false); });\n  var mark = function (t, s) {\n    var seen = {}, n = 0;\n    while (t !== 0 && self.valid(t, s) && !seen[t + ':' + s] && n <= self.sectors) {\n      seen[t + ':' + s] = 1; n++; self.setFree(t, s, false);\n      var b = self.block(t, s); t = b[0]; s = b[1];\n    }\n  };\n  this.files().forEach(function (e) {\n    if (!e.closed) { self.data[self.entryOffset(e) + 2] = 0; removed++; return; }\n    if (e.t) mark(e.t, e.s);\n    if (e.type === 4 && e.sideT) mark(e.sideT, e.sideS);\n  });\n  return removed;\n};\nP.copyFile = function (newName, oldNames) {\n  var parts = [], type = 0, self = this;\n  oldNames.forEach(function (n) {\n    var e = self.find(n, 0);\n    if (!e) throw DiskError(62, 'FILE NOT FOUND');\n    type = type || e.type; parts.push(self.readFile(e));\n  });\n  var len = parts.reduce(function (a, p) { return a + p.length; }, 0), all = new Uint8Array(len), p = 0;\n  parts.forEach(function (x) { all.set(x, p); p += x.length; });\n  return this.writeFile(newName, all, type || 2);\n};\n\n\/* ---------- directory come programma BASIC (quello che il drive manda con LOAD\"$\",8) ---------- *\/\nP.dirProgram = function (patBytes) {\n  var h = this.headerInfo(), out = [0x01, 0x04], addr = 0x0401;\n  function line(num, text) {\n    var len = 2 + 2 + text.length + 1, next = addr + len;\n    out.push(next & 255, next >> 8, num & 255, num >> 8);\n    for (var i = 0; i < text.length; i++) out.push(text[i]);\n    out.push(0); addr = next;\n  }\n  var raw = h.raw.slice(0, 16).map(function (b) { return b === 0xA0 ? 0x20 : b; });\n  var hdr = [0x12, 0x22].concat(raw, [0x22, 0x20, h.raw[18], h.raw[19], 0x20, h.raw[21], h.raw[22]]);\n  line(0, hdr);\n  this.files().forEach(function (e) {\n    if (patBytes && patBytes.length && !wildMatch(patBytes, e.name)) return;\n    var t = [], sp = e.blocks < 10 ? 3 : e.blocks < 100 ? 2 : e.blocks < 1000 ? 1 : 0, i;\n    for (i = 0; i < sp; i++) t.push(0x20);\n    t.push(0x22); t = t.concat(e.name); t.push(0x22);\n    for (i = e.name.length; i < 16; i++) t.push(0x20);\n    t.push(e.closed ? 0x20 : 0x2A);\n    t = t.concat(strToPet(e.typeName));\n    t.push(e.locked ? 0x3C : 0x20);\n    while (t.length < 27) t.push(0x20);\n    line(e.blocks, t);\n  });\n  var foot = strToPet('BLOCKS FREE.');\n  while (foot.length < 25) foot.push(0x20);\n  line(this.blocksFree(), foot);\n  out.push(0, 0);\n  return new Uint8Array(out);\n};\n\n\/* ---------- lettura di una directory in formato BASIC (dal drive vero o simulato) ---------- *\/\nfunction parseDirProgram(bytes) {\n  var res = {name: [], id: [], files: [], free: null, ok: false}, p = 2, guard = 0;\n  while (p + 2 <= bytes.length && guard++ < 2000) {\n    var link = bytes[p] | (bytes[p + 1] << 8);\n    if (link === 0) { res.ok = true; break; }\n    if (p + 4 > bytes.length) break;\n    var num = bytes[p + 2] | (bytes[p + 3] << 8), q = p + 4, text = [];\n    while (q < bytes.length && bytes[q] !== 0) text.push(bytes[q++]);\n    p = q + 1;\n    var q1 = text.indexOf(0x22), q2 = q1 >= 0 ? text.indexOf(0x22, q1 + 1) : -1;\n    if (text.indexOf(0x12) >= 0 && res.files.length === 0 && q2 > 0) {\n      res.name = text.slice(q1 + 1, q2);\n      while (res.name.length && res.name[res.name.length - 1] === 0x20) res.name.pop();\n      res.id = text.slice(q2 + 1).filter(function (b) { return b !== 0x20; });\n      var it = text.slice(q2 + 1); while (it.length && it[0] === 0x20) it.shift(); while (it.length && it[it.length - 1] === 0x20) it.pop();\n      res.idText = it;\n    } else if (q2 > 0) {\n      var rest = text.slice(q2 + 1), r = 0;\n      while (r < rest.length && rest[r] === 0x20) r++;\n      var splat = rest[r] === 0x2A; if (splat) r++;\n      var tn = petToStr(rest.slice(r, r + 3));\n      res.files.push({name: text.slice(q1 + 1, q2), blocks: num, typeName: tn, type: TYPES.indexOf(tn), closed: !splat, locked: rest[r + 3] === 0x3C});\n    } else if (\/BLOCKS FREE\/.test(petToStr(text))) res.free = num;\n    if (p >= bytes.length) { res.ok = true; break; }\n  }\n  return res;\n}\n\n\/* ---------- BASIC V2: elenco di un programma ---------- *\/\nvar TOKENS = ['END','FOR','NEXT','DATA','INPUT#','INPUT','DIM','READ','LET','GOTO','RUN','IF','RESTORE','GOSUB','RETURN','REM','STOP','ON','WAIT','LOAD','SAVE','VERIFY','DEF','POKE','PRINT#','PRINT','CONT','LIST','CLR','CMD','SYS','OPEN','CLOSE','GET','NEW','TAB(','TO','FN','SPC(','THEN','NOT','STEP','+','-','*','\/','\u2191','AND','OR','>','=','<','SGN','INT','ABS','USR','FRE','POS','SQR','RND','LOG','EXP','COS','SIN','TAN','ATN','PEEK','LEN','STR$','VAL','ASC','CHR$','LEFT$','RIGHT$','MID$','GO'];\nvar CTRL = {0x05:'{wht}',0x0D:'{return}',0x0E:'{lower}',0x11:'{down}',0x12:'{rvon}',0x13:'{home}',0x14:'{del}',0x1C:'{red}',0x1D:'{rght}',0x1E:'{grn}',0x1F:'{blu}',\n  0x81:'{orng}',0x8E:'{upper}',0x90:'{blk}',0x91:'{up}',0x92:'{rvof}',0x93:'{clr}',0x94:'{inst}',0x95:'{brn}',0x96:'{lred}',0x97:'{gry1}',0x98:'{gry2}',0x99:'{lgrn}',0x9A:'{lblu}',0x9B:'{gry3}',0x9C:'{pur}',0x9D:'{left}',0x9E:'{yel}',0x9F:'{cyn}'};\nfunction basicList(prg, maxLines) {\n  if (prg.length < 4) return null;\n  var load = prg[0] | (prg[1] << 8), p = 2, addr = load, lines = [], guard = 0;\n  maxLines = maxLines || 2000;\n  while (p + 2 <= prg.length && guard++ < maxLines) {\n    var link = prg[p] | (prg[p + 1] << 8);\n    if (link === 0) return lines.length ? {load: load, lines: lines, end: p + 2} : null;\n    if (p + 4 > prg.length) break;\n    var num = prg[p + 2] | (prg[p + 3] << 8), q = p + 4, s = '', quote = false, rem = false, sys = false;\n    while (q < prg.length && prg[q] !== 0) {\n      var b = prg[q++];\n      if (b === 0x9E && !quote) sys = true;\n      if (b === 0x22) quote = !quote;\n      if (!quote && !rem && b >= 0x80 && b <= 0xCB) { s += TOKENS[b - 0x80]; if (b === 0x8F) rem = true; }\n      else if (b === 0xFF && !quote) s += '\u03c0';\n      else if (CTRL[b] && (quote || rem)) s += CTRL[b];\n      else if (b < 0x20 || (b >= 0x80 && b < 0xA0)) s += '{$' + b.toString(16).padStart(2, '0') + '}';\n      else s += petChar(b);\n    }\n    if (q >= prg.length) break;\n    lines.push(num + ' ' + s);\n    var next = addr + (q + 1 - p);\n    p = q + 1;\n    \/\/ il puntatore deve indicare la riga seguente: se non torna, dopo c'\u00e8 codice macchina (programmi con SYS)\n    if (link !== next) {\n      \/\/ alla prima riga accettiamo un puntatore sbagliato solo se c'\u00e8 SYS (programmi compressi con una riga di avvio)\n      if (lines.length === 1 && !sys) return null;\n      return {load: load, lines: lines, end: p, partial: true};\n    }\n    addr = next;\n  }\n  return lines.length ? {load: load, lines: lines, end: p, partial: true} : null;\n}\n\/\/ tokenizzazione minima, per creare il disco dimostrativo\nfunction basicTokenize(lines, load) {\n  load = load || 0x0801;\n  var out = [load & 255, load >> 8], addr = load;\n  lines.forEach(function (l) {\n    var m = \/^(\\d+)\\s*(.*)$\/.exec(l), num = +m[1], src = m[2], body = [], i = 0, quote = false;\n    while (i < src.length) {\n      var c = src[i];\n      if (c === '\"') { quote = !quote; body.push(0x22); i++; continue; }\n      if (!quote) {\n        var tok = -1, best = 0;\n        for (var k = 0; k < TOKENS.length; k++) {\n          var t = TOKENS[k];\n          if (t.length > best && src.substr(i, t.length).toUpperCase() === t) { tok = k; best = t.length; }\n        }\n        if (tok >= 0) { body.push(0x80 + tok); i += best; if (tok === 0x0F) { body = body.concat(strToPet(src.slice(i))); i = src.length; } continue; }\n      }\n      body = body.concat(strToPet(c)); i++;\n    }\n    var len = 4 + body.length + 1, next = addr + len;\n    out.push(next & 255, next >> 8, num & 255, num >> 8);\n    out = out.concat(body); out.push(0); addr = next;\n  });\n  out.push(0, 0);\n  return new Uint8Array(out);\n}\n\n\/* ---------- file P00 (PC64): nome originale nell'intestazione ---------- *\/\nfunction parseP00(bytes) {\n  var sig = 'C64File';\n  for (var i = 0; i < 7; i++) if (bytes[i] !== sig.charCodeAt(i)) return null;\n  var nm = Array.prototype.slice.call(bytes, 8, 24);\n  var z = nm.indexOf(0); if (z >= 0) nm = nm.slice(0, z);\n  return {name: nm, data: bytes.slice(26)};\n}\n\nRF.Disk = Disk;\nRF.DiskError = DiskError;\nRF.pet = {toStr: petToStr, char: petChar, fromStr: strToPet, fromPcName: pcNameToC64, toPcName: c64NameToPc, eq: bytesEq, match: wildMatch};\nRF.parseDirProgram = parseDirProgram;\nRF.basicList = basicList;\nRF.basicTokenize = basicTokenize;\nRF.parseP00 = parseP00;\nRF.FILE_TYPES = TYPES;\n})(window.RFFloppy = window.RFFloppy || {});\n\n\/* RetroFixer \u00b7 Floppy C64 \u00b7 xum1541 \/ ZoomFloppy via WebUSB\n * Xum1541: trasporto USB (stesso protocollo del plugin xum1541 di OpenCBM, vedi PROTOCOLLO-XUM1541.md).\n * Cbm:     le funzioni cbm_* di OpenCBM (listen, talk, open, close, raw_read, raw_write, exec_command, device_status\u2026).\n * Drive:   operazioni ad alto livello (directory, formattazione, cancellazione, rinomina, validazione, PRG, blocchi, dischi interi).\n *\/\n(function (RF) {\n'use strict';\n\nvar X = {\n  VID: 0x16D0, PID: 0x0504, EP_IN: 3, EP_OUT: 4, IFACE: 0,\n  ECHO: 0, INIT: 1, RESET: 2, SHUTDOWN: 3, GITREV: 6,\n  READ: 8, WRITE: 9, GET_EOI: 23, CLEAR_EOI: 24,\n  CBM: 0x10, F_TALK: 0x01, F_ATN: 0x02,\n  BUSY: 1, READY: 2, ERROR: 3, DOING_RESET: 0x01, MAX_XFER: 32768, MIN_VERSION: 7,\n  CTRL_TIMEOUT: 1650\n};\nRF.XUM = X;\n\nfunction sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }\nfunction withTimeout(p, ms, what) {\n  return new Promise(function (res, rej) {\n    var t = setTimeout(function () { rej(new Error('Timeout USB: ' + what)); }, ms);\n    p.then(function (v) { clearTimeout(t); res(v); }, function (e) { clearTimeout(t); rej(e); });\n  });\n}\nfunction u8(dv) { return new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength); }\nfunction hex(b) { return Array.prototype.map.call(b, function (x) { return x.toString(16).padStart(2, '0'); }).join(' '); }\nfunction concat(parts) {\n  var n = 0, i; for (i = 0; i < parts.length; i++) n += parts[i].length;\n  var out = new Uint8Array(n), p = 0;\n  for (i = 0; i < parts.length; i++) { out.set(parts[i], p); p += parts[i].length; }\n  return out;\n}\nRF.util = {sleep: sleep, hex: hex, concat: concat};\n\n\/* ======================= trasporto USB ======================= *\/\nfunction Xum1541(device, log) {\n  this.dev = device; this.log = log || function () {}; this.version = 0; this.caps = 0; this.status = 0; this.gitrev = '';\n  this.simulated = !!device.simulated;\n}\nXum1541.supported = function () { return typeof navigator !== 'undefined' && !!navigator.usb; };\nXum1541.filters = [{vendorId: X.VID, productId: X.PID}];\n\/\/ chiede all'utente di scegliere la xum1541 (finestra di Chrome \/ Edge)\nXum1541.request = function () { return navigator.usb.requestDevice({filters: Xum1541.filters}); };\n\/\/ dispositivi gi\u00e0 autorizzati in visite precedenti\nXum1541.known = function () {\n  if (!Xum1541.supported()) return Promise.resolve([]);\n  return navigator.usb.getDevices().then(function (l) { return l.filter(function (d) { return d.vendorId === X.VID && d.productId === X.PID; }); });\n};\nvar XP = Xum1541.prototype;\nXP.ctrlIn = function (req, len) {\n  return withTimeout(this.dev.controlTransferIn({requestType: 'class', recipient: 'device', request: req, value: 0, index: 0}, len), X.CTRL_TIMEOUT, 'control ' + req)\n    .then(function (r) { if (r.status !== 'ok') throw new Error('USB control ' + req + ': ' + r.status); return u8(r.data); });\n};\nXP.ctrlOut = function (req, timeout) {\n  return withTimeout(this.dev.controlTransferOut({requestType: 'class', recipient: 'device', request: req, value: 0, index: 0}), timeout || X.CTRL_TIMEOUT, 'control ' + req)\n    .then(function (r) { if (r.status !== 'ok') throw new Error('USB control ' + req + ': ' + r.status); });\n};\nXP.bulkOut = function (data) {\n  var self = this;\n  return this.dev.transferOut(X.EP_OUT, data).then(function (r) {\n    if (r.status === 'stall') return self.dev.clearHalt('out', X.EP_OUT).then(function () { throw new Error('USB: endpoint OUT in stallo'); });\n    if (r.status !== 'ok') throw new Error('USB OUT: ' + r.status);\n    return r.bytesWritten;\n  });\n};\nXP.bulkIn = function (len) {\n  var self = this;\n  return this.dev.transferIn(X.EP_IN, len).then(function (r) {\n    if (r.status === 'stall') return self.dev.clearHalt('in', X.EP_IN).then(function () { throw new Error('USB: endpoint IN in stallo'); });\n    if (r.status !== 'ok' && r.status !== 'babble') throw new Error('USB IN: ' + r.status);\n    return r.data ? u8(r.data) : new Uint8Array(0);\n  });\n};\nXP.open = async function () {\n  var d = this.dev;\n  if (!d.opened) await d.open();\n  if (!d.configuration || d.configuration.configurationValue !== 1) await d.selectConfiguration(1);\n  await d.claimInterface(X.IFACE);\n  try { await d.selectAlternateInterface(X.IFACE, 0); } catch (e) { \/* alcuni sistemi lo rifiutano se \u00e8 gi\u00e0 0 *\/ }\n  var info = await this.ctrlIn(X.INIT, 8);\n  if (info.length < 2) throw new Error('La xum1541 non ha risposto a INIT');\n  this.version = info[0]; this.caps = info[1]; this.status = info[2] || 0;\n  this.log('usb', 'INIT \u2192 firmware v' + this.version + ', capacit\u00e0 0x' + this.caps.toString(16) + ', stato 0x' + this.status.toString(16));\n  if (this.version < X.MIN_VERSION) throw new Error('Firmware xum1541 troppo vecchio (v' + this.version + '): serve almeno la v' + X.MIN_VERSION + '.');\n  if (this.status & X.DOING_RESET) {\n    this.log('usb', 'la sessione precedente era stata interrotta: clear halt sugli endpoint');\n    await d.clearHalt('in', X.EP_IN); await d.clearHalt('out', X.EP_OUT);\n  }\n  if (this.version >= 8) {\n    try { var g = await this.ctrlIn(X.GITREV, 8); this.gitrev = String.fromCharCode.apply(null, g).replace(\/\\0.*$\/, ''); } catch (e) {}\n  }\n};\nXP.close = async function () {\n  try { await this.ctrlOut(X.SHUTDOWN, 1000); } catch (e) {}\n  try { await this.dev.releaseInterface(X.IFACE); } catch (e) {}\n  try { await this.dev.close(); } catch (e) {}\n};\nXP.waitStatus = async function () {\n  for (;;) {\n    var s = await this.bulkIn(3);\n    if (s.length !== 3) throw new Error('USB: stato di ' + s.length + ' byte');\n    if (s[0] === X.BUSY) continue;\n    if (s[0] === X.READY) return s[1] | (s[2] << 8);\n    if (s[0] === X.ERROR) return -1;\n    throw new Error('USB: stato sconosciuto ' + s[0]);\n  }\n};\nXP.ioctl = async function (cmd, addr, sec) {\n  await this.bulkOut(new Uint8Array([cmd, addr || 0, sec || 0, 0]));\n  return this.waitStatus();\n};\n\/\/ xum1541_write: restituisce i byte scritti sul bus (0 = nessun drive ha risposto)\nXP.write = async function (mode, data) {\n  var len = data.length;\n  if (len > 0xFFFF) throw new Error('write: massimo 65535 byte per comando');\n  await this.bulkOut(new Uint8Array([X.WRITE, mode, len & 255, len >> 8]));\n  for (var p = 0; p < len; p += X.MAX_XFER) {\n    var n = await this.bulkOut(data.subarray(p, Math.min(len, p + X.MAX_XFER)));\n    if (n < Math.min(X.MAX_XFER, len - p)) break;\n  }\n  return (mode & 0xF0) === X.CBM ? this.waitStatus() : len;\n};\n\/\/ xum1541_read: legge fino a len byte; meno byte = EOI o timeout del bus\nXP.read = async function (mode, len) {\n  if (len > 0xFFFF) throw new Error('read: massimo 65535 byte per comando');\n  await this.bulkOut(new Uint8Array([X.READ, mode, len & 255, len >> 8]));\n  var parts = [], got = 0;\n  while (got < len) {\n    var want = Math.min(X.MAX_XFER, len - got), d = await this.bulkIn(want);\n    parts.push(d); got += d.length;\n    if (d.length < want) break;\n  }\n  return concat(parts);\n};\nXP.reset = function () { return this.ctrlOut(X.RESET, 4000); };\nRF.Xum1541 = Xum1541;\n\n\/* ======================= cbm_* (come OpenCBM) ======================= *\/\nfunction Cbm(xum, log) {\n  this.x = xum; this.log = log || xum.log; this.queue = Promise.resolve(); this.abort = false;\n}\nvar CP = Cbm.prototype;\n\/\/ tutte le operazioni passano da qui, una alla volta (WebUSB \u00e8 asincrono)\nCP.exclusive = function (fn) {\n  var run = this.queue.then(fn, fn);\n  this.queue = run.catch(function () {});\n  return run;\n};\nCP.atn = async function (bytes, talk) {\n  var n = await this.x.write(X.CBM | X.F_ATN | (talk ? X.F_TALK : 0), new Uint8Array(bytes));\n  return n > 0 ? 0 : 1; \/\/ come OpenCBM: 0 = riuscito\n};\nCP.listen = function (dev, sa) { this.log('bus', 'LISTEN ' + dev + ',' + sa); return this.atn([0x20 | dev, 0x60 | sa]); };\nCP.talk = function (dev, sa) { this.log('bus', 'TALK ' + dev + ',' + sa); return this.atn([0x40 | dev, 0x60 | sa], true); };\nCP.unlisten = function () { this.log('bus', 'UNLISTEN'); return this.atn([0x3F]); };\nCP.untalk = function () { this.log('bus', 'UNTALK'); return this.atn([0x5F]); };\nCP.rawWrite = function (data) { return this.x.write(X.CBM, data instanceof Uint8Array ? data : new Uint8Array(data)); };\nCP.rawRead = function (len) { return this.x.read(X.CBM, len); };\nCP.getEoi = function () { return this.x.ioctl(X.GET_EOI).then(function (v) { return v === 1; }); };\nCP.clearEoi = function () { return this.x.ioctl(X.CLEAR_EOI); };\nCP.reset = function () { this.log('bus', 'RESET del bus IEC'); return this.x.reset(); };\n\/\/ cbm_open: OPEN sa + nome del file + UNLISTEN\nCP.open = async function (dev, sa, name) {\n  this.log('bus', 'OPEN ' + dev + ',' + sa + (name && name.length ? ',\"' + RF.pet.toStr(name) + '\"' : ''));\n  if (await this.atn([0x20 | dev, 0xF0 | sa])) return 1;\n  var rv = 0;\n  if (name && name.length) {\n    rv = (await this.rawWrite(name)) !== name.length ? 1 : 0;\n  }\n  await this.unlisten();\n  return rv;\n};\nCP.close = async function (dev, sa) {\n  this.log('bus', 'CLOSE ' + dev + ',' + sa);\n  var rv = await this.atn([0x20 | dev, 0xE0 | sa]);\n  await this.unlisten();\n  return rv;\n};\n\/\/ cbm_exec_command: LISTEN dev,15 + comando + UNLISTEN\nCP.execCommand = async function (dev, cmd) {\n  var bytes = typeof cmd === 'string' ? RF.pet.fromStr(cmd) : cmd;\n  this.log('cmd', '\"' + RF.pet.toStr(bytes) + '\"');\n  var rv = await this.listen(dev, 15);\n  if (rv === 0) rv = (await this.rawWrite(bytes)) !== bytes.length ? 1 : 0;\n  await this.unlisten();\n  return rv;\n};\n\/\/ cbm_device_status: TALK dev,15 + lettura fino al CR + UNTALK. Riprova finch\u00e9 il drive \u00e8 occupato.\nCP.deviceStatus = async function (dev, waitMs) {\n  var deadline = Date.now() + (waitMs || 4000), buf = [];\n  if (await this.talk(dev, 15)) { await this.untalk(); return parseStatus(null); }\n  for (;;) {\n    var d = await this.rawRead(64);\n    for (var i = 0; i < d.length; i++) buf.push(d[i]);\n    if (buf.indexOf(13) >= 0) break;\n    if (d.length < 64) { if (await this.getEoi()) break; }\n    if (d.length === 0 && Date.now() > deadline) break;\n    if (this.abort || buf.length > 200) break;\n  }\n  await this.untalk();\n  var st = parseStatus(buf);\n  this.log('st', st.text);\n  return st;\n};\nfunction parseStatus(buf) {\n  if (!buf || !buf.length) return {code: 99, msg: 'DRIVER ERROR', t: 0, s: 0, text: '99, DRIVER ERROR,00,00', none: true};\n  var cr = buf.indexOf(13), s = String.fromCharCode.apply(null, cr >= 0 ? buf.slice(0, cr) : buf);\n  var m = \/^\\s*(\\d+)\\s*,\\s*([^,]*?)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\/.exec(s);\n  if (!m) return {code: parseInt(s, 10) || 99, msg: s, t: 0, s: 0, text: s};\n  return {code: +m[1], msg: m[2], t: +m[3], s: +m[4], text: s};\n}\nCbm.parseStatus = parseStatus;\nRF.Cbm = Cbm;\n\n\/* ======================= operazioni ad alto livello ======================= *\/\nfunction DriveError(st, what) {\n  var e = new Error((what ? what + ': ' : '') + st.text);\n  e.status = st; return e;\n}\nfunction Drive(cbm, dev) { this.cbm = cbm; this.dev = dev || 8; this.model = ''; this.dosText = ''; }\nvar DP = Drive.prototype;\nDP.run = function (fn) { var self = this; return self.cbm.exclusive(function () { self.cbm.abort = false; return fn.call(self, self.cbm); }); };\nDP.status = function (waitMs) { var self = this; return this.run(function (c) { return c.deviceStatus(self.dev, waitMs); }); };\n\/\/ comando DOS libero con lettura dello stato (waitMs: quanto aspettare un drive occupato)\nDP.command = function (cmd, waitMs) {\n  var self = this;\n  return this.run(async function (c) {\n    if (await c.execCommand(self.dev, cmd)) throw DriveError({code: 99, text: tr('notPresent', self.dev)});\n    return c.deviceStatus(self.dev, waitMs || 4000);\n  });\n};\nfunction tr(k, a) { return (RF.msg && RF.msg(k, a)) || k; }\nfunction checkSt(st, what) { if (st.code >= 20 && st.code !== 73) throw DriveError(st, what); return st; }\nDP.identify = function () {\n  var self = this;\n  return this.run(async function (c) {\n    if (await c.execCommand(self.dev, 'UJ')) throw DriveError({code: 99, text: tr('notPresent', self.dev)});\n    await sleep(c.x.simulated ? 50 : 2200);\n    var st = await c.deviceStatus(self.dev, 6000);\n    self.setModel(st);\n    return st;\n  });\n};\nDP.setModel = function (st) {\n  if (st.code !== 73) return;\n  this.dosText = st.msg;\n  var m = \/(1541|1570|1571|1581|2031|4040|8050|8250|1001)\/.exec(st.msg);\n  this.model = m ? m[1] : '';\n};\nDP.geometry = function () { return this.model === '1581' ? 'd81' : this.model === '1571' ? 'd71' : 'd64'; };\n\n\/\/ LOAD\"$\",8: directory come programma BASIC, poi decodificata\nDP.directory = function (onProgress) {\n  var self = this;\n  return this.run(async function (c) {\n    var raw = await self._readFile(c, 0, RF.pet.fromStr('$'), onProgress);\n    var dir = RF.parseDirProgram(raw);\n    dir.raw = raw;\n    return dir;\n  });\n};\n\/\/ lettura di un file intero da un canale gi\u00e0 scelto (sa 0 = LOAD, 2 = file con tipo)\nDP._readFile = async function (c, sa, name, onProgress, sizeHint) {\n  var dev = this.dev;\n  if (await c.open(dev, sa, name)) throw DriveError({code: 99, text: tr('notPresent', dev)});\n  var st = await c.deviceStatus(dev, 8000);\n  if (st.code >= 20) { await c.close(dev, sa); throw DriveError(st); }\n  var parts = [], got = 0, idle = 0, t0 = Date.now();\n  if (await c.talk(dev, sa)) { await c.close(dev, sa); throw DriveError({code: 99, text: tr('notPresent', dev)}); }\n  for (;;) {\n    if (c.abort) { await c.untalk(); await c.close(dev, sa); throw new Error(tr('aborted')); }\n    var d = await c.rawRead(254);\n    if (d.length) { parts.push(d); got += d.length; idle = 0; if (onProgress) onProgress(got, sizeHint || 0, Date.now() - t0); }\n    if (d.length < 254) {\n      if (await c.getEoi()) break;\n      if (d.length === 0 && ++idle > 8) break; \/\/ nessun dato per ~8 s: il drive non risponde\n    }\n  }\n  await c.untalk();\n  await c.close(dev, sa);\n  var data = concat(parts);\n  if (!got) { var st2 = await c.deviceStatus(dev); if (st2.code >= 20) throw DriveError(st2); }\n  return data;\n};\nDP.readFile = function (nameBytes, typeName, onProgress, sizeHint) {\n  var self = this, t = (typeName || 'PRG')[0];\n  return this.run(function (c) {\n    var name = nameBytes.concat(RF.pet.fromStr(',' + t + ',R'));\n    return self._readFile(c, 2, name, onProgress, sizeHint);\n  });\n};\n\/\/ scrittura di un file (SEQ\/PRG\/USR) sul canale 2, a pezzi per mostrare l'avanzamento\nDP.writeFile = function (nameBytes, data, typeName, opts, onProgress) {\n  var self = this;\n  return this.run(function (c) { return self._writeFile(c, nameBytes, data, typeName, opts, onProgress); });\n};\nDP._writeFile = async function (c, nameBytes, data, typeName, opts, onProgress) {\n  var t = (typeName || 'PRG')[0], dev = this.dev;\n  opts = opts || {};\n  {\n    if (opts.replace) {\n      await c.execCommand(dev, RF.pet.fromStr('S0:').concat(nameBytes));\n      await c.deviceStatus(dev, 8000);\n    }\n    var name = RF.pet.fromStr('0:').concat(nameBytes, RF.pet.fromStr(',' + t + ',W'));\n    if (await c.open(dev, 2, name)) throw DriveError({code: 99, text: tr('notPresent', dev)});\n    var st = await c.deviceStatus(dev, 8000);\n    if (st.code >= 20) { await c.close(dev, 2); throw DriveError(st); }\n    var t0 = Date.now();\n    if (await c.listen(dev, 2)) throw DriveError({code: 99, text: tr('notPresent', dev)});\n    for (var p = 0; p < data.length; p += 254) {\n      if (c.abort) { await c.unlisten(); await c.close(dev, 2); throw new Error(tr('aborted')); }\n      var chunk = data.subarray(p, Math.min(data.length, p + 254));\n      var n = await c.rawWrite(chunk);\n      if (n !== chunk.length) { await c.unlisten(); await c.close(dev, 2); throw DriveError(await c.deviceStatus(dev)); }\n      if (onProgress) onProgress(p + chunk.length, data.length, Date.now() - t0);\n    }\n    await c.unlisten();\n    await c.close(dev, 2);\n    return checkSt(await c.deviceStatus(dev, 8000));\n  }\n};\nfunction nm(bytes) { return Array.isArray(bytes) ? bytes : Array.prototype.slice.call(bytes); }\nDP.scratch = function (nameBytes) {\n  return this.command(RF.pet.fromStr('S0:').concat(nm(nameBytes)), 15000).then(function (st) { return checkSt(st); });\n};\nDP.rename = function (oldBytes, newBytes) {\n  return this.command(RF.pet.fromStr('R0:').concat(nm(newBytes), [0x3D], nm(oldBytes)), 8000).then(function (st) { return checkSt(st); });\n};\nDP.validate = function () { return this.command('V0', 120000).then(function (st) { return checkSt(st); }); };\nDP.initialize = function () { return this.command('I0', 8000); };\n\/\/ N:nome,id (completa, scrive ogni traccia) oppure N:nome (rapida, solo su dischi gi\u00e0 formattati)\nDP.format = function (nameBytes, idBytes) {\n  var cmd = RF.pet.fromStr('N0:').concat(nm(nameBytes));\n  if (idBytes && idBytes.length) cmd = cmd.concat([0x2C], nm(idBytes));\n  return this.command(cmd, 240000).then(function (st) { return checkSt(st); });\n};\n\n\/* ---------- blocchi (U1 \/ U2 \/ B-P) e dischi interi ---------- *\/\nDP._blockOpen = async function (c) {\n  if (await c.open(this.dev, 2, [0x23])) throw DriveError({code: 99, text: tr('notPresent', this.dev)});\n  checkSt(await c.deviceStatus(this.dev));\n};\nDP._blockRead = async function (c, t, s) {\n  var dev = this.dev;\n  await c.execCommand(dev, 'U1:2 0 ' + t + ' ' + s);\n  var st = await c.deviceStatus(dev, 8000);\n  if (st.code >= 20) return {st: st, data: null};\n  await c.execCommand(dev, 'B-P:2 0');\n  await c.talk(dev, 2);\n  var d = await c.rawRead(256);\n  await c.untalk();\n  return {st: st, data: d.length === 256 ? d : null};\n};\nDP._blockWrite = async function (c, t, s, data) {\n  var dev = this.dev;\n  await c.execCommand(dev, 'B-P:2 0');\n  await c.listen(dev, 2);\n  var n = await c.rawWrite(data);\n  await c.unlisten();\n  if (n !== 256) return checkSt(await c.deviceStatus(dev));\n  await c.execCommand(dev, 'U2:2 0 ' + t + ' ' + s);\n  return c.deviceStatus(dev, 8000);\n};\nDP.readBlock = function (t, s) {\n  var self = this;\n  return this.run(async function (c) {\n    await self._blockOpen(c);\n    try { var r = await self._blockRead(c, t, s); checkSt(r.st); return r.data; }\n    finally { await c.close(self.dev, 2); }\n  });\n};\n\/\/ copia del disco intero in un'immagine (blocco per blocco, bus standard: lento)\nDP.readDisk = function (fmtId, onProgress) {\n  var self = this;\n  return this.run(async function (c) {\n    var img = RF.Disk.blank(fmtId, RF.pet.fromStr('LETTURA'), RF.pet.fromStr('00'));\n    var total = img.sectors, done = 0, errors = [], t0 = Date.now();\n    await self._blockOpen(c);\n    try {\n      for (var t = 1; t <= img.tracks; t++) {\n        for (var s = 0; s < img.spt(t); s++) {\n          if (c.abort) throw new Error(tr('aborted'));\n          var r = await self._blockRead(c, t, s);\n          if (r.data) img.writeBlock(t, s, r.data); else { img.block(t, s).fill(0); errors.push({t: t, s: s, st: r.st}); }\n          done++;\n          if (onProgress) onProgress(done, total, Date.now() - t0, t, s);\n        }\n      }\n    } finally { await c.close(self.dev, 2); }\n    img.readErrors = errors;\n    return img;\n  });\n};\n\/\/ scrittura di un'immagine sul disco: formatta con lo stesso nome e ID, poi scrive i blocchi (solo quelli usati, se richiesto)\nDP.writeDisk = function (img, opts, onProgress) {\n  var self = this;\n  opts = opts || {};\n  return this.run(async function (c) {\n    var h = img.headerInfo(), dev = self.dev, t0 = Date.now(), list = [], t, s;\n    if (opts.format !== false) {\n      if (onProgress) onProgress(0, 1, 0, 0, 0, 'format');\n      await c.execCommand(dev, RF.pet.fromStr('N0:').concat(h.name.length ? h.name : RF.pet.fromStr('DISCO'), [0x2C], h.id));\n      checkSt(await c.deviceStatus(dev, 240000), 'N');\n    }\n    for (t = 1; t <= img.tracks; t++) for (s = 0; s < img.spt(t); s++) {\n      if (opts.usedOnly === false || t > img.dosTracks() || img.isReserved(t) || !img.isFree(t, s)) list.push([t, s]);\n    }\n    await self._blockOpen(c);\n    try {\n      for (var i = 0; i < list.length; i++) {\n        if (c.abort) throw new Error(tr('aborted'));\n        var st = await self._blockWrite(c, list[i][0], list[i][1], img.readBlock(list[i][0], list[i][1]));\n        checkSt(st, 'U2 ' + list[i][0] + '\/' + list[i][1]);\n        if (onProgress) onProgress(i + 1, list.length, Date.now() - t0, list[i][0], list[i][1]);\n      }\n    } finally { await c.close(dev, 2); }\n    await c.execCommand(dev, 'I0'); await c.deviceStatus(dev);\n    return list.length;\n  });\n};\n\/* ---------- separatori nella directory (file DEL da 0 blocchi) ----------\n * Il DOS non ha un comando per creare file DEL: si scrive la voce direttamente nel blocco della directory (U1 \/ U2).\n * La voce punta al primo blocco della directory, che \u00e8 gi\u00e0 occupato: un \"V\" (validate) non cambia niente.\n * Con \"locked\" (DEL<) il DOS non la cancella con S:, che altrimenti libererebbe i blocchi della directory.\n *\/\nDP._dirStart = async function (c) {\n  if (this.model === '1581') return [40, 3];\n  if (this.model) return [18, 1];\n  var r = await this._blockRead(c, 18, 0);\n  if (r.data && r.data[2] === 0x41) return [18, 1];\n  r = await this._blockRead(c, 40, 0);\n  if (r.data && r.data[2] === 0x44) return [40, 3];\n  throw DriveError({code: 74, text: tr('noDosDisk')});\n};\nDP._dirSlots = async function (c, start) {\n  var out = [], t = start[0], s = start[1], seen = {};\n  while (t && !seen[t + ':' + s] && out.length < 40) {\n    seen[t + ':' + s] = 1;\n    var r = await this._blockRead(c, t, s);\n    checkSt(r.st);\n    if (!r.data) throw DriveError(r.st);\n    out.push({t: t, s: s, data: r.data});\n    t = r.data[0]; s = r.data[1];\n  }\n  return out;\n};\nfunction padName(bytes) { var a = new Array(16).fill(0xA0); for (var i = 0; i < 16 && i < bytes.length; i++) a[i] = bytes[i]; return a; }\nDP.addSeparator = function (nameBytes, locked) {\n  var self = this, dev = this.dev;\n  return this.run(async function (c) {\n    await self._blockOpen(c);\n    var start = await self._dirStart(c), secs = await self._dirSlots(c, start), slot = null;\n    function find() {\n      for (var k = 0; k < secs.length && !slot; k++) for (var i = 0; i < 8; i++) if (secs[k].data[i * 32 + 2] === 0) { slot = {sec: secs[k], o: i * 32}; break; }\n    }\n    find();\n    if (!slot) {\n      \/\/ directory piena: un file temporaneo fa allungare la directory al DOS, poi lo cancelliamo e resta la voce libera\n      await c.close(dev, 2);\n      var tmp = RF.pet.fromStr('RF-SEP-TMP');\n      await self._writeFile(c, tmp, new Uint8Array([1, 8, 0, 0]), 'PRG', {});\n      await c.execCommand(dev, RF.pet.fromStr('S0:').concat(tmp)); checkSt(await c.deviceStatus(dev, 8000));\n      await self._blockOpen(c);\n      secs = await self._dirSlots(c, start); find();\n      if (!slot) throw DriveError({code: 72, text: '72,DISK FULL,00,00'});\n    }\n    try {\n      var d = slot.sec.data, o = slot.o;\n      for (var j = 2; j < 32; j++) d[o + j] = 0;\n      d[o + 2] = locked ? 0xC0 : 0x80; d[o + 3] = start[0]; d[o + 4] = start[1];\n      d.set(padName(nameBytes), o + 5);\n      checkSt(await self._blockWrite(c, slot.sec.t, slot.sec.s, d), 'U2');\n    } finally { await c.close(dev, 2); }\n    await c.execCommand(dev, 'I0');\n    return checkSt(await c.deviceStatus(dev, 8000));\n  });\n};\n\/\/ toglie una voce DEL dalla directory senza liberare blocchi (S: libererebbe la catena a cui punta)\nDP.removeDel = function (nameBytes) {\n  var self = this, dev = this.dev;\n  return this.run(async function (c) {\n    await self._blockOpen(c);\n    try {\n      var secs = await self._dirSlots(c, await self._dirStart(c));\n      for (var k = 0; k < secs.length; k++) for (var i = 0; i < 8; i++) {\n        var d = secs[k].data, o = i * 32;\n        if (d[o + 2] === 0 || (d[o + 2] & 7) !== 0) continue;\n        var n = Array.prototype.slice.call(d, o + 5, o + 21);\n        while (n.length && n[n.length - 1] === 0xA0) n.pop();\n        if (!RF.pet.eq(n, nm(nameBytes))) continue;\n        d[o + 2] = 0;\n        checkSt(await self._blockWrite(c, secs[k].t, secs[k].s, d), 'U2');\n        await c.close(dev, 2);\n        await c.execCommand(dev, 'I0');\n        return checkSt(await c.deviceStatus(dev, 8000));\n      }\n    } catch (e) { await c.close(dev, 2); throw e; }\n    await c.close(dev, 2);\n    throw DriveError({code: 62, text: '62,FILE NOT FOUND,00,00'});\n  });\n};\nRF.Drive = Drive;\n})(window.RFFloppy = window.RFFloppy || {});\n\n\/* RetroFixer \u00b7 Floppy C64 \u00b7 xum1541 e drive simulati\n * FakeXum1541: imita un USBDevice di WebUSB con il firmware della xum1541 (comandi di controllo, READ, WRITE, IOCTL, stato).\n * FakeDrive:   un drive 1541 \/ 1571 \/ 1581 sul bus IEC (LISTEN, TALK, OPEN, CLOSE, canale 15, LOAD\"$\", file, blocchi) con un disco in memoria.\n * Cos\u00ec tutto il codice di cbm.js gira identico a quando c'\u00e8 l'hardware vero: cambia solo l'oggetto \"device\".\n *\/\n(function (RF) {\n'use strict';\nvar X = RF.XUM, pet = RF.pet;\nfunction sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }\nfunction dv(bytes) { var a = new Uint8Array(bytes); return new DataView(a.buffer); }\n\nvar DOS = {\n  '1541': {msg: 'CBM DOS V2.6 1541', fmt: 'd64'},\n  '1571': {msg: 'CBM DOS V3.0 1571', fmt: 'd71'},\n  '1581': {msg: 'COPYRIGHT CBM DOS V10 1581', fmt: 'd81'}\n};\n\/\/ tempi del drive in modalit\u00e0 \"realistica\" (millisecondi): stime, da confrontare con l'hardware vero\nvar REAL = {byteMs: 1000 \/ 450, fmtFull: {'1541': 80000, '1571': 75000, '1581': 45000}, fmtQuick: 3000, validate: 12000, scratch: 900, block: 120, open: 250, reset: 1600};\n\n\/* ======================= drive sul bus IEC ======================= *\/\nfunction FakeDrive(opts) {\n  opts = opts || {};\n  this.model = opts.model || '1541';\n  this.dev = opts.dev || 8;\n  this.speed = opts.speed || 'instant';\n  this.writeProtect = !!opts.writeProtect;\n  this.disk = opts.disk || null;\n  this.powered = true;\n  this.onChange = opts.onChange || function () {};\n  this.onLed = opts.onLed || function () {};\n  this.reset(true);\n}\nvar F = FakeDrive.prototype;\nF.reset = function (power) {\n  this.listening = false; this.talking = false; this.sa = 0; this.mode = null; this.nameBuf = [];\n  this.cmdBuf = []; this.channels = {}; this.busyUntil = 0;\n  this.setError(73, DOS[this.model].msg, 0, 0);\n  if (!power) this.busy(REAL.reset, true);\n};\nF.setModel = function (m) { this.model = m; this.reset(true); };\nF.insert = function (disk) { this.disk = disk; this.channels = {}; this.setError(0, ' OK', 0, 0); this.onChange(); };\nF.eject = function () { this.disk = null; this.channels = {}; this.onChange(); };\nF.setError = function (code, msg, t, s) {\n  var txt = String(code).padStart(2, '0') + ',' + msg + ',' + String(t || 0).padStart(2, '0') + ',' + String(s || 0).padStart(2, '0');\n  this.err = {code: code, bytes: pet.fromStr(txt).concat([13]), pos: 0};\n};\nF.ok = function () { this.setError(0, ' OK', 0, 0); };\nF.fail = function (e) {\n  if (e && e.code) this.setError(e.code, e.code === 1 ? 'FILES SCRATCHED' : e.message, e.t, e.s);\n  else this.setError(99, 'DRIVER ERROR', 0, 0);\n};\n\/\/ visible: in modalit\u00e0 istantanea le operazioni lunghe (formattazione, validazione, reset) durano comunque un attimo, per vedere il LED\nF.busy = function (ms, visible) {\n  var d = this.speed === 'real' ? ms : (visible ? 400 : 0);\n  if (!d) return;\n  this.busyUntil = Math.max(this.busyUntil, Date.now() + d); this.onLed(true);\n};\nF.isBusy = function () { var b = Date.now() < this.busyUntil; if (!b) this.onLed(false); return b; };\nF.needDisk = function () { if (!this.disk) throw RF.DiskError(74, 'DRIVE NOT READY'); };\nF.needWrite = function () { this.needDisk(); if (this.writeProtect) throw RF.DiskError(26, 'WRITE PROTECT ON'); };\n\n\/\/ byte sotto ATN: restituisce quanti byte sono stati accettati\nF.atn = function (bytes) {\n  if (!this.powered) return 0;\n  for (var i = 0; i < bytes.length; i++) {\n    var b = bytes[i], cmd = b & 0xE0, arg = b & 0x1F;\n    if (b === 0x3F) { if (this.listening) this.endListen(); this.listening = false; }\n    else if (b === 0x5F) { this.talking = false; }\n    else if (cmd === 0x20) { this.listening = arg === this.dev; this.talking = false; this.mode = null; }\n    else if (cmd === 0x40) { this.talking = arg === this.dev; if (this.talking) this.listening = false; this.mode = null; }\n    else if ((b & 0xF0) === 0x60) { this.sa = b & 0x0F; this.mode = 'data'; if (this.sa === 15 && this.listening) this.cmdBuf = []; }\n    else if ((b & 0xF0) === 0xE0) { if (this.listening) this.closeCh(b & 0x0F); this.mode = null; }\n    else if ((b & 0xF0) === 0xF0) { if (this.listening) { this.sa = b & 0x0F; this.mode = 'open'; this.nameBuf = []; } }\n  }\n  return bytes.length;\n};\n\/\/ byte di dati dal PC\nF.write = function (bytes) {\n  if (!this.powered || !this.listening) return 0;\n  if (this.mode === 'open') { for (var i = 0; i < bytes.length; i++) this.nameBuf.push(bytes[i]); return bytes.length; }\n  if (this.sa === 15) { for (var j = 0; j < bytes.length; j++) this.cmdBuf.push(bytes[j]); return bytes.length; }\n  var ch = this.channels[this.sa];\n  if (!ch || !ch.write) { this.setError(61, 'FILE NOT OPEN', 0, 0); return 0; }\n  if (ch.buffer) { for (var k = 0; k < bytes.length; k++) { ch.data[ch.pos & 255] = bytes[k]; ch.pos = (ch.pos + 1) & 255; } }\n  else for (var m = 0; m < bytes.length; m++) ch.out.push(bytes[m]);\n  return bytes.length;\n};\n\/\/ lettura dal drive che parla: {data, eoi}; nessun dato = timeout del bus (come il firmware dopo 1 s)\nF.read = function (len) {\n  if (!this.powered || !this.talking) return {data: [], eoi: false};\n  var out = [], eoi = false;\n  if (this.sa === 15) {\n    var e = this.err;\n    while (out.length < len && e.pos < e.bytes.length) out.push(e.bytes[e.pos++]);\n    if (e.pos >= e.bytes.length) { eoi = true; this.ok(); }\n    return {data: out, eoi: eoi};\n  }\n  var ch = this.channels[this.sa];\n  if (!ch || !ch.read) return {data: [], eoi: false};\n  if (ch.buffer) {\n    while (out.length < len) { out.push(ch.data[ch.pos & 255]); ch.pos = (ch.pos + 1) & 255; if (ch.pos === 0) { eoi = true; break; } }\n  } else {\n    while (out.length < len && ch.pos < ch.data.length) out.push(ch.data[ch.pos++]);\n    if (ch.pos >= ch.data.length) eoi = true;\n  }\n  return {data: out, eoi: eoi};\n};\n\nF.endListen = function () {\n  if (this.mode === 'open') { this.mode = null; this.doOpen(this.sa, this.nameBuf); }\n  else if (this.sa === 15 && this.cmdBuf.length) { var c = this.cmdBuf; this.cmdBuf = []; this.doCommand(c); }\n};\n\n\/\/ nome con prefisso e opzioni: \"@0:NOME,P,W\"\nfunction splitName(bytes) {\n  var b = bytes.slice(), replace = false, type = 0, mode = '';\n  while (b.length && b[b.length - 1] === 13) b.pop();\n  if (b[0] === 0x40) { replace = true; b.shift(); }\n  var colon = b.indexOf(0x3A);\n  if (colon >= 0 && colon <= 2) b = b.slice(colon + 1);\n  var parts = [], cur = [];\n  b.forEach(function (x) { if (x === 0x2C) { parts.push(cur); cur = []; } else cur.push(x); });\n  parts.push(cur);\n  for (var i = 1; i < parts.length; i++) {\n    var c = String.fromCharCode(parts[i][0] || 0);\n    if ('SPUL'.indexOf(c) >= 0 && i === 1) type = {S: 1, P: 2, U: 3, L: 4}[c];\n    else if ('RWAM'.indexOf(c) >= 0) mode = c;\n  }\n  return {name: parts[0], replace: replace, type: type, mode: mode};\n}\n\nF.doOpen = function (sa, nameBytes) {\n  try {\n    if (sa === 15) { if (nameBytes.length) this.doCommand(nameBytes); return; }\n    this.busy(REAL.open);\n    if (nameBytes[0] === 0x23) { \/\/ \"#\": buffer per i comandi di blocco\n      this.channels[sa] = {buffer: true, read: true, write: true, data: new Uint8Array(256), pos: 0};\n      this.ok(); return;\n    }\n    this.needDisk();\n    if (nameBytes[0] === 0x24 && (sa === 0)) { \/\/ \"$\" o \"$0:MODELLO\"\n      var c = nameBytes.indexOf(0x3A), pat = c >= 0 ? nameBytes.slice(c + 1) : [];\n      this.channels[sa] = {read: true, data: this.disk.dirProgram(pat), pos: 0};\n      this.ok(); return;\n    }\n    var p = splitName(nameBytes);\n    if (!p.name.length) throw RF.DiskError(34, 'SYNTAX ERROR');\n    var writing = sa === 1 || p.mode === 'W';\n    if (writing) {\n      this.needWrite();\n      for (var i = 0; i < p.name.length; i++) if (p.name[i] === 0x2A || p.name[i] === 0x3F) throw RF.DiskError(33, 'SYNTAX ERROR');\n      if (!p.replace && this.disk.findExact(p.name)) throw RF.DiskError(63, 'FILE EXISTS');\n      this.channels[sa] = {write: true, out: [], name: p.name, type: p.type || 2, replace: p.replace};\n      this.ok(); return;\n    }\n    var want = sa < 2 ? 2 : p.type;\n    var e = this.disk.find(p.name, 0);\n    if (!e) throw RF.DiskError(62, 'FILE NOT FOUND');\n    if (want && e.type !== want) throw RF.DiskError(64, 'FILE TYPE MISMATCH');\n    this.channels[sa] = {read: true, data: this.disk.readFile(e), pos: 0};\n    this.ok();\n  } catch (err) { this.fail(err); }\n};\nF.closeCh = function (sa) {\n  var ch = this.channels[sa];\n  delete this.channels[sa];\n  if (!ch || !ch.write || ch.buffer) return;\n  try {\n    this.needWrite();\n    this.disk.writeFile(ch.name, new Uint8Array(ch.out), ch.type, {replace: ch.replace});\n    this.ok(); this.onChange();\n  } catch (err) { this.fail(err); }\n};\n\nfunction asc(bytes) { return String.fromCharCode.apply(null, bytes); }\nfunction nums(str) { return (str.match(\/\\d+\/g) || []).map(Number); }\nfunction splitNames(bytes) { var out = [], cur = []; bytes.forEach(function (b) { if (b === 0x2C) { out.push(cur); cur = []; } else cur.push(b); }); out.push(cur); return out.filter(function (x) { return x.length; }); }\nfunction afterColon(bytes) { var i = bytes.indexOf(0x3A); return i >= 0 ? bytes.slice(i + 1) : bytes.slice(1); }\n\nF.doCommand = function (raw) {\n  var b = raw.slice();\n  while (b.length && (b[b.length - 1] === 13 || b[b.length - 1] === 0x20)) b.pop();\n  var s = asc(b), c0 = s[0], m;\n  try {\n    if (b.length > 58) throw RF.DiskError(32, 'SYNTAX ERROR');\n    if (\/^U[J:;I9]\/.test(s) || \/^UI\/.test(s)) { this.reset(false); return; }\n    if (\/^(U1|UA|B-R)\/.test(s)) { this.needDisk(); this.blockCmd(s, 'r'); return; }\n    if (\/^(U2|UB|B-W)\/.test(s)) { this.needWrite(); this.blockCmd(s, 'w'); return; }\n    if (\/^B-P\/.test(s)) { var n = nums(s.slice(3)), ch = this.channels[n[0]]; if (!ch || !ch.buffer) throw RF.DiskError(70, 'NO CHANNEL'); ch.pos = (n[1] || 0) & 255; this.ok(); return; }\n    if (\/^M-[RWE]\/.test(s)) { this.ok(); return; }\n    if (c0 === 'I') { this.needDisk(); this.ok(); return; }\n    if (c0 === 'V') { this.needWrite(); this.busy(REAL.validate, true); this.disk.validate(); this.ok(); this.onChange(); return; }\n    if (c0 === 'N') {\n      this.needWrite();\n      var parts = splitNames(afterColon(b)), name = parts[0] || [], id = parts[1];\n      if (!name.length) throw RF.DiskError(34, 'SYNTAX ERROR');\n      if (id && id.length) {\n        if (!this.disk || this.disk.fmt.id !== DOS[this.model].fmt) this.disk = RF.Disk.blank(DOS[this.model].fmt, name, id);\n        else this.disk.format(name, id);\n        this.busy(REAL.fmtFull[this.model], true);\n      } else {\n        if (!this.disk) throw RF.DiskError(74, 'DRIVE NOT READY');\n        this.disk.quickFormat(name);\n        this.busy(REAL.fmtQuick, true);\n      }\n      this.ok(); this.onChange(); return;\n    }\n    if (c0 === 'S') {\n      this.needWrite();\n      var pats = splitNames(afterColon(b));\n      if (!pats.length) throw RF.DiskError(34, 'SYNTAX ERROR');\n      var k = this.disk.scratch(pats);\n      this.busy(REAL.scratch * Math.max(1, k));\n      this.setError(1, 'FILES SCRATCHED', k, 0); this.onChange(); return;\n    }\n    if (c0 === 'R') {\n      this.needWrite();\n      var rb = afterColon(b), eq = rb.indexOf(0x3D);\n      if (eq < 0) throw RF.DiskError(34, 'SYNTAX ERROR');\n      var oldB = rb.slice(eq + 1), ci = oldB.indexOf(0x3A); if (ci >= 0 && ci <= 1) oldB = oldB.slice(ci + 1);\n      this.disk.rename(rb.slice(0, eq), oldB);\n      this.busy(REAL.scratch); this.ok(); this.onChange(); return;\n    }\n    if (c0 === 'C') {\n      this.needWrite();\n      var cb = afterColon(b), ce = cb.indexOf(0x3D);\n      if (ce < 0) throw RF.DiskError(34, 'SYNTAX ERROR');\n      var srcs = splitNames(cb.slice(ce + 1)).map(function (x) { var i = x.indexOf(0x3A); return i >= 0 && i <= 1 ? x.slice(i + 1) : x; });\n      if (this.disk.findExact(cb.slice(0, ce))) throw RF.DiskError(63, 'FILE EXISTS');\n      this.disk.copyFile(cb.slice(0, ce), srcs); this.busy(REAL.validate \/ 2); this.ok(); this.onChange(); return;\n    }\n    throw RF.DiskError(31, 'SYNTAX ERROR');\n  } catch (err) { this.fail(err); }\n};\nF.blockCmd = function (s, dir) {\n  var n = nums(s.replace(\/^(U1|U2|UA|UB|B-R|B-W)\/, '')), ch = this.channels[n[0]];\n  if (!ch || !ch.buffer) throw RF.DiskError(70, 'NO CHANNEL');\n  var t = n[2], sec = n[3];\n  if (!this.disk.valid(t, sec)) throw RF.DiskError(66, 'ILLEGAL TRACK OR SECTOR', t, sec);\n  if (dir === 'r') { ch.data.set(this.disk.readBlock(t, sec)); ch.pos = 0; }\n  else { this.disk.writeBlock(t, sec, ch.data); this.onChange(); }\n  this.busy(REAL.block);\n  this.ok();\n};\n\n\/* ======================= xum1541 simulata (stessa interfaccia di un USBDevice) ======================= *\/\nfunction FakeXum1541(drive, opts) {\n  opts = opts || {};\n  this.simulated = true;\n  this.drive = drive;\n  this.vendorId = X.VID; this.productId = X.PID;\n  this.productName = 'xum1541 (simulata)'; this.manufacturerName = 'RetroFixer'; this.serialNumber = '0';\n  this.opened = false; this.configuration = null; this.claimed = false;\n  this.inQ = []; this.pending = null; this.eoi = false; this.halted = false;\n  this.usbDelay = opts.usbDelay || 0;\n}\nvar U = FakeXum1541.prototype;\nfunction result(status, data) { return {status: status, data: data ? dv(data) : new DataView(new ArrayBuffer(0)), bytesWritten: 0}; }\nU.open = async function () { this.opened = true; };\nU.close = async function () { this.opened = false; this.claimed = false; };\nU.selectConfiguration = async function (v) { this.configuration = {configurationValue: v}; };\nU.claimInterface = async function (i) { if (!this.opened) throw new Error('device chiuso'); this.claimed = true; };\nU.releaseInterface = async function () { this.claimed = false; };\nU.selectAlternateInterface = async function () {};\nU.clearHalt = async function () { this.halted = false; };\nU.reset = async function () {};\nU.controlTransferIn = async function (setup, len) {\n  if (setup.requestType !== 'class') return result('stall');\n  if (setup.request === X.INIT) return result('ok', [8, 0x03, 0, 0, 0, 0, 0, 0].slice(0, len));\n  if (setup.request === X.ECHO) return result('ok', [0]);\n  if (setup.request === X.GITREV) return result('ok', pet.fromStr('SIMULAT').concat([0]));\n  return result('stall');\n};\nU.controlTransferOut = async function (setup) {\n  if (setup.request === X.RESET) { this.drive.reset(false); this.inQ = []; this.pending = null; await sleep(this.drive.speed === 'real' ? 800 : 20); }\n  return {status: 'ok', bytesWritten: 0};\n};\nU.transferOut = async function (ep, data) {\n  if (ep !== X.EP_OUT) return {status: 'stall', bytesWritten: 0};\n  var b = new Uint8Array(data.buffer ? data.buffer.slice(data.byteOffset || 0, (data.byteOffset || 0) + data.byteLength) : data);\n  if (this.usbDelay) await sleep(this.usbDelay);\n  if (this.pending) {\n    var p = this.pending;\n    for (var i = 0; i < b.length; i++) p.buf.push(b[i]);\n    if (p.buf.length >= p.len) { this.pending = null; await this.doWrite(p.flags, p.buf); }\n    return {status: 'ok', bytesWritten: b.length};\n  }\n  if (b.length !== 4) return {status: 'stall', bytesWritten: 0};\n  var cmd = b[0], len = b[2] | (b[3] << 8);\n  if (cmd === X.WRITE) { if (len === 0) this.inQ.push([X.READY, 0, 0]); else this.pending = {flags: b[1], len: len, buf: []}; }\n  else if (cmd === X.READ) await this.doRead(len);\n  else if (cmd === X.GET_EOI) this.inQ.push([X.READY, this.eoi ? 1 : 0, 0]);\n  else if (cmd === X.CLEAR_EOI) { this.eoi = false; this.inQ.push([X.READY, 0, 0]); }\n  else if (cmd >= 16) this.inQ.push([X.READY, 0, 0]);\n  else return {status: 'stall', bytesWritten: 0};\n  return {status: 'ok', bytesWritten: 4};\n};\nU.doWrite = async function (flags, buf) {\n  this.eoi = false; \/\/ come il firmware: ogni scrittura azzera EOI\n  var n;\n  if (flags & X.F_ATN) n = this.drive.atn(buf);\n  else {\n    \/\/ un drive occupato (per esempio mentre formatta) accetta i dati solo quando ha finito\n    while (this.drive.isBusy()) await sleep(Math.min(200, this.drive.busyUntil - Date.now() + 1));\n    n = this.drive.write(buf);\n    if (this.drive.speed === 'real') await sleep(buf.length * REAL.byteMs);\n  }\n  this.inQ.push([X.READY, n & 255, n >> 8]);\n};\nU.doRead = async function (len) {\n  if (this.eoi) { this.inQ.push([]); return; }\n  \/\/ il firmware aspetta al massimo 1 s che il drive inizi a parlare\n  if (this.drive.isBusy()) {\n    var wait = Math.min(1000, this.drive.busyUntil - Date.now());\n    await sleep(Math.max(0, wait));\n    if (this.drive.isBusy()) { this.inQ.push([]); return; }\n  }\n  var r = this.drive.read(len);\n  if (r.eoi) this.eoi = true;\n  if (this.drive.speed === 'real' && r.data.length) await sleep(r.data.length * REAL.byteMs);\n  this.inQ.push(r.data);\n};\nU.transferIn = async function (ep, len) {\n  if (ep !== X.EP_IN) return result('stall');\n  if (this.usbDelay) await sleep(this.usbDelay);\n  var guard = 0;\n  while (!this.inQ.length) { await sleep(5); if (++guard > 2000) return result('stall'); }\n  var d = this.inQ[0];\n  if (d.length > len) { this.inQ[0] = d.slice(len); d = d.slice(0, len); }\n  else this.inQ.shift();\n  return result('ok', d);\n};\n\n\/* ======================= disco dimostrativo ======================= *\/\nfunction demoDisk(fmtId) {\n  var P = pet.fromStr, d = RF.Disk.blank(fmtId || 'd64', P('RETROFIXER DEMO'), P('RF'));\n  d.writeFile(P('CIAO RETROFIXER'), RF.basicTokenize([\n    '10 PRINT CHR$(147)\"CIAO DA RETROFIXER.IT!\"',\n    '20 FOR I=1 TO 15:POKE 53280,I:POKE 53281,I-1',\n    '30 FOR J=1 TO 150:NEXT J,I',\n    '40 POKE 53280,14:POKE 53281,6',\n    '50 PRINT \"QUESTO FILE ARRIVA DAL DRIVE SIMULATO.\"'\n  ]), 2);\n  var music = new Uint8Array(4098); music[0] = 0x00; music[1] = 0x10;\n  for (var i = 2; i < music.length; i++) music[i] = (i * 37 + (i >> 5)) & 255;\n  d.writeFile(P('MUSICA SID'), music, 2);\n  d.writeFile(P('TESTO NOTE'), new Uint8Array(P('APPUNTI DI PROVA SU UN FILE SEQUENZIALE.\\rSECONDA RIGA.\\r')), 1);\n  var big = new Uint8Array(30 * 254); big[0] = 0x01; big[1] = 0x08;\n  for (var k = 2; k < big.length; k++) big[k] = (k * 13) & 255;\n  d.writeFile(P('GIOCO GRANDE'), big, 2);\n  d.addSeparator(P('----------------'), true);\n  return d;\n}\n\nRF.FakeDrive = FakeDrive;\nRF.FakeXum1541 = FakeXum1541;\nRF.demoDisk = demoDisk;\nRF.DOS_MODELS = DOS;\nRF.SIM_TIMING = REAL;\n})(window.RFFloppy = window.RFFloppy || {});\n\n\/* RetroFixer \u00b7 Floppy C64 \u00b7 lettura minima di archivi ZIP (molte release di CSDb sono .zip)\n * Metodi \"stored\" e \"deflate\", decompressi con DecompressionStream del browser: nessuna libreria esterna.\n *\/\n(function (RF) {\n'use strict';\nasync function inflateRaw(bytes) {\n  var ds = new DecompressionStream('deflate-raw');\n  var stream = new Blob([bytes]).stream().pipeThrough(ds);\n  return new Uint8Array(await new Response(stream).arrayBuffer());\n}\nRF.readZip = async function (u8, maxFiles) {\n  var v = new DataView(u8.buffer, u8.byteOffset, u8.byteLength), eocd = -1;\n  for (var i = u8.length - 22; i >= Math.max(0, u8.length - 65557); i--) if (v.getUint32(i, true) === 0x06054b50) { eocd = i; break; }\n  if (eocd < 0) throw new Error('ZIP non valido');\n  var count = v.getUint16(eocd + 10, true), p = v.getUint32(eocd + 16, true), out = [], dec = new TextDecoder();\n  for (var k = 0; k < count && k < (maxFiles || 500); k++) {\n    if (v.getUint32(p, true) !== 0x02014b50) break;\n    var method = v.getUint16(p + 10, true), csize = v.getUint32(p + 20, true), usize = v.getUint32(p + 24, true);\n    var nlen = v.getUint16(p + 28, true), xlen = v.getUint16(p + 30, true), clen = v.getUint16(p + 32, true), loc = v.getUint32(p + 42, true);\n    var name = dec.decode(u8.subarray(p + 46, p + 46 + nlen));\n    p += 46 + nlen + xlen + clen;\n    if (\/\\\/$\/.test(name) || usize > 8 * 1024 * 1024) continue;\n    var start = loc + 30 + v.getUint16(loc + 26, true) + v.getUint16(loc + 28, true), body = u8.subarray(start, start + csize), data;\n    if (method === 0) data = body.slice();\n    else if (method === 8) data = await inflateRaw(body);\n    else continue;\n    out.push({name: name.split('\/').pop(), path: name, data: data});\n  }\n  return out;\n};\n})(window.RFFloppy = window.RFFloppy || {});\n\n\/* RetroFixer \u00b7 Floppy C64 \u00b7 interfaccia del widget *\/\n(function (RF) {\n'use strict';\nvar EN = \/^en\/i.test(document.documentElement.lang || '');\nvar T = {\n it: {\n  title: 'Floppy C64 dal browser',\n  intro: 'Collega un drive 1541, 1571 o 1581 al computer con una xum1541 o una ZoomFloppy e gestisci i dischi da questa pagina: directory, formattazione, cancellazione, rinomina, copia di file PRG dal PC e da CSDb, immagini D64. Non hai l\\'hardware? Prova tutto con il drive simulato.',\n  connT: 'Collegamento', mUsb: '\u25cf Drive reale', mUsbS: 'xum1541 o ZoomFloppy via USB: scrive sui floppy veri', mSim: 'Simulazione', mSimS: 'senza hardware: nessun floppy vero viene toccato',\n  flagOn: '<b>\u25cf ONLINE<\/b> Drive {m} reale collegato con la xum1541: le operazioni leggono e scrivono il floppy nel drive.',\n  flagOff: '<b>\u25cb OFFLINE<\/b> Nessun drive collegato. Collega la xum1541 qui sopra, oppure prova la simulazione; le immagini D64 si gestiscono comunque nella scheda \u00abImmagine disco\u00bb.',\n  flagSim: '<b>SIMULAZIONE<\/b> Disco virtuale in memoria: nessun floppy vero viene letto o scritto. Per lavorare sui dischi veri serve la xum1541; senza, puoi provare i comandi e gestire le immagini D64.',\n  trySim: 'Prova la simulazione', drvTSim: 'Disco simulato', drvTOff: 'Drive non collegato',\n  bSep: 'Separatore\u2026', bImgSep: '+ Separatore', opSep: 'Scrittura del separatore', sepT: 'Aggiungi un separatore',\n  sepB: '<p>Aggiunge alla directory una riga come <code>0 \"----------------\" DEL<\/code>: un file DEL da 0 blocchi, che non occupa spazio. Va nel primo posto libero della directory, di solito in fondo.<\/p><p>Sul drive vero si scrive direttamente il blocco della directory (comandi <code>U1<\/code>\/<code>U2<\/code>): funziona con Chrome ed Edge, come tutto il collegamento USB. Nelle immagini D64, D71 e D81 funziona in ogni browser.<\/p>',\n  sepStyle: 'Stile', sepDash: '- - - trattini', sepLine: '\u2500\u2500\u2500 linea grafica PETSCII', sepEq: '=== uguale', sepStar: '*** asterischi', sepCustom: 'testo personalizzato', sepText: 'Testo (solo per \u00abpersonalizzato\u00bb, max 16)', sepLock: 'protetto \u00ab<\u00bb (consigliato: il DOS non lo cancella con S:)', bSepGo: 'Aggiungi',\n  sepDone: 'Separatore aggiunto.', delSepB: '<p>Togliere il separatore <b>\u00ab{n}\u00bb<\/b>? Viene cancellata solo la sua riga nella directory.<\/p>', imgChanged: 'Immagine modificata: scaricala per salvarla sul PC.', aRm: '\u2715 Togli',\n  usbGo: 'Collega la xum1541', usbAgain: 'Ricollega l\\'ultima', usbOff: 'Scollega',\n  usbNo: 'Questo browser non supporta WebUSB: per collegare la xum1541 serve <b>Chrome<\/b>, <b>Edge<\/b> o un altro browser Chromium su computer. Il drive simulato e il visualizzatore di immagini funzionano ovunque.',\n  usbNoSecure: 'WebUSB funziona solo su pagine https.',\n  usbIdle: 'Collega la xum1541 (o la ZoomFloppy) a una porta USB, accendi il drive e premi <b>Collega<\/b>: il browser mostrer\u00e0 l\\'elenco dei dispositivi compatibili.',\n  usbKnown: 'Questo browser ha gi\u00e0 il permesso per una xum1541: puoi ricollegarla senza sceglierla di nuovo.',\n  usbOn: 'Collegata: <b>{n}<\/b>, firmware v{v}{g}.',\n  usbCancel: 'Nessun dispositivo scelto.',\n  usbClaim: 'Il browser vede la xum1541 ma non riesce ad aprirla. Su Windows serve il driver <b>WinUSB<\/b> (vedi sotto); su Linux una regola udev; ovunque, chiudi gli altri programmi che la usano (OpenCBM, cbmctrl, VICE).',\n  usbLost: 'La xum1541 \u00e8 stata scollegata.',\n  helpT: 'Prima di collegarla: browser, driver e permessi',\n  helpL: '<li><b>Browser<\/b>: Chrome, Edge, Opera o Brave su computer. Firefox e Safari non hanno WebUSB.<\/li>' +\n   '<li><b>Windows<\/b>: Chrome pu\u00f2 usare solo dispositivi con il driver <b>WinUSB<\/b>. Scarica <a href=\"https:\/\/zadig.akeo.ie\/\" target=\"_blank\" rel=\"noopener\">Zadig<\/a>, menu <i>Options \u2192 List All Devices<\/i>, scegli la xum1541 (VID <code>16D0<\/code>, PID <code>0504<\/code>), seleziona <i>WinUSB<\/i> e premi <i>Replace Driver<\/i>. Se prima usavi OpenCBM con un altro driver (libusb-win32 o libusbK), controlla che funzioni ancora.<\/li>' +\n   '<li><b>Linux<\/b>: crea <code>\/etc\/udev\/rules.d\/45-xum1541.rules<\/code> con<br><code>SUBSYSTEM==\"usb\", ATTR{idVendor}==\"16d0\", ATTR{idProduct}==\"0504\", TAG+=\"uaccess\"<\/code><br>poi <code>sudo udevadm control --reload-rules<\/code> e ricollega la xum1541.<\/li>' +\n   '<li><b>macOS<\/b>: non servono driver.<\/li>' +\n   '<li>La xum1541 pu\u00f2 essere usata da un solo programma alla volta: chiudi OpenCBM, cbmctrl e simili.<\/li>' +\n   '<li>Senza caricatori veloci il bus seriale \u00e8 lento: leggere o scrivere un disco intero richiede vari minuti.<\/li>',\n  simModel: 'Drive', simDisk: 'Disco', dDemo: 'dimostrativo', dBlank: 'vuoto, formattato', dNone: 'nessun disco',\n  simSpeed: 'Velocit\u00e0', spInst: 'istantanea', spReal: 'realistica (bus seriale)', simWp: 'disco protetto da scrittura', simDl: 'Scarica il disco simulato',\n  simNote: 'Il drive simulato risponde come una xum1541 vera con un drive collegato: la pagina usa esattamente gli stessi comandi. Con la velocit\u00e0 <b>realistica<\/b> i tempi sono una stima del bus seriale senza turbo.',\n  devNr: 'Numero', bStatus: 'Leggi stato', bIdent: 'Identifica (UJ)', bReset: 'Reset bus',\n  drvT: 'Disco nel drive', bDir: 'Directory', bFmt: 'Formatta\u2026', bVal: 'Valida', bCmd: 'Comando DOS\u2026', bRdDisk: 'Disco \u2192 immagine', bWrDisk: 'Immagine \u2192 disco', bStop: 'Interrompi',\n  dropDrv: 'Trascina qui file PRG o SEQ per scriverli sul disco',\n  tFiles: 'File del PC', tImg: 'Immagine disco', tLog: 'Registro',\n  dropBig: 'Trascina qui i file dal tuo computer', dropSmall: 'PRG, P00, SEQ, immagini D64, D71, D81 e archivi ZIP',\n  bPick: 'Scegli file', bPickDir: 'Apri una cartella', bWrAll: 'Scrivi tutti sul disco', bClr: 'Svuota elenco',\n  csPh: 'Cerca su CSDb: titolo, oppure numero o link di una release', bSearch: 'Cerca',\n  csNote: 'Ricerca e download passano dal server di RetroFixer, perch\u00e9 CSDb non permette al browser di scaricare direttamente i file. Si possono aprire solo i file ospitati su csdb.dk.',\n  logBus: 'mostra i comandi del bus e USB', bCopy: 'Copia', bClear: 'Svuota',\n  credits: '<p>Il protocollo USB della xum1541 \u00e8 quello documentato dal progetto <a href=\"https:\/\/github.com\/OpenCBM\/OpenCBM\" target=\"_blank\" rel=\"noopener\">OpenCBM<\/a> (firmware di Nate Lawson): questa pagina ne \u00e8 un\\'implementazione indipendente in JavaScript con WebUSB. I file di <a href=\"https:\/\/csdb.dk\/\" target=\"_blank\" rel=\"noopener\">CSDb<\/a> restano dei rispettivi autori.<\/p><p>Prototipo: con l\\'hardware vero vanno ancora verificati driver, tempi e copie di dischi interi. I dischi originali protetti da copia non si possono duplicare con i comandi del DOS.<\/p>',\n  stUsb: 'WebUSB: <b>disponibile<\/b>', stUsbNo: 'WebUSB: <i>non disponibile in questo browser<\/i>', stFs: 'cartelle: <b>s\u00ec<\/b>', stFsNo: 'cartelle: solo lettura',\n  stFmt: 'D64 \u00b7 D71 \u00b7 D81 \u00b7 PRG \u00b7 P00 \u00b7 SEQ \u00b7 ZIP',\n  noConn: 'NESSUN DRIVE COLLEGATO.', noConn2: 'Scegli il drive simulato oppure collega una xum1541.',\n  loading: 'LOAD\"$\",{d}', searching: 'SEARCHING FOR $', ready: 'READY.',\n  subDir: '{n} file \u00b7 {f} blocchi liberi', sub0: 'disco vuoto \u00b7 {f} blocchi liberi',\n  aDl: '\u2b07 PC', aRen: '\u270e Rinomina', aDel: '\u2715 Cancella', aView: 'Mostra', aDrive: '\u2192 Drive', aExt: '\u2b07 Estrai',\n  notPresent: 'Nessun drive risponde al numero {0}: controlla che sia acceso, collegato e con il numero giusto.', aborted: 'Operazione interrotta.',\n  busy: 'Aspetta che finisca l\\'operazione in corso.',\n  opDir: 'Lettura della directory', opRead: 'Lettura di \u00ab{n}\u00bb', opWrite: 'Scrittura di \u00ab{n}\u00bb', opFmt: 'Formattazione', opVal: 'Validazione', opDel: 'Cancellazione di \u00ab{n}\u00bb',\n  opRen: 'Rinomina', opRdDisk: 'Lettura del disco intero', opWrDisk: 'Scrittura dell\\'immagine sul disco', opFmtFirst: 'Formattazione prima della scrittura\u2026', opId: 'Identificazione del drive', opSt: 'Lettura dello stato', opRst: 'Reset del bus',\n  eta: '{d} di {t} \u00b7 {r} \u00b7 mancano {e}', etaBlk: 'blocco {d} di {t} (traccia {tt}, settore {ss}) \u00b7 mancano {e}', rate: '{v} byte\/s',\n  fmtT: 'Formatta il disco', fmtB: '<p class=\"warn\">Tutto il contenuto del disco verr\u00e0 cancellato.<\/p><p>La formattazione completa scrive ogni traccia e su un 1541 dura pi\u00f9 di un minuto. Quella rapida cancella solo directory e BAM e funziona solo su dischi gi\u00e0 formattati.<\/p>',\n  fName: 'Nome del disco (max 16 caratteri)', fId: 'ID (2 caratteri)', fQuick: 'formattazione rapida (senza ID)', bFmtGo: 'Formatta',\n  valT: 'Valida il disco', valB: '<p>Il comando <code>V<\/code> ricostruisce la mappa dei blocchi liberi (BAM) leggendo tutti i file, e toglie i file rimasti aperti (quelli con l\\'asterisco, come <code>*PRG<\/code>).<\/p><p>Su un disco con protezioni o con file \"speciali\" pu\u00f2 liberare blocchi usati da caricatori particolari: in quel caso \u00e8 meglio non farlo.<\/p>', bValGo: 'Valida',\n  delT: 'Cancella file', delB: '<p>Cancellare <b>\u00ab{n}\u00bb<\/b> dal disco? Il comando \u00e8 <code>S0:{n}<\/code>.<\/p>', bDelGo: 'Cancella',\n  renT: 'Rinomina file', renF: 'Nuovo nome', bRenGo: 'Rinomina',\n  cmdT: 'Comando DOS', cmdB: '<p>Il comando viene mandato sul canale 15 e la risposta del drive appare sotto. Esempi: <code>I0<\/code> inizializza, <code>UJ<\/code> reset del drive, <code>S0:NOME<\/code>, <code>R0:NUOVO=VECCHIO<\/code>, <code>C0:COPIA=ORIGINALE<\/code>.<\/p>', cmdF: 'Comando', bSend: 'Invia',\n  rdT: 'Disco \u2192 immagine', rdB: '<p>Legge tutti i blocchi del disco con i comandi <code>U1<\/code> e crea un\\'immagine da scaricare o aprire nel visualizzatore.<\/p><p>{e}<\/p>', rdFmt: 'Formato', bRdGo: 'Leggi il disco',\n  wrT: 'Immagine \u2192 disco', wrB: '<p>Scrive l\\'immagine <b>\u00ab{n}\u00bb<\/b> sul disco nel drive con i comandi <code>U2<\/code>.<\/p><p class=\"warn\">Il contenuto attuale del disco verr\u00e0 perso.<\/p><p>{e}<\/p>', wrFmt: 'formatta prima con lo stesso nome e ID dell\\'immagine', wrUsed: 'scrivi solo i blocchi usati (pi\u00f9 veloce)', bWrGo: 'Scrivi sul disco',\n  wrNoImg: 'Apri prima un\\'immagine D64, D71 o D81 nella scheda \u00abImmagine disco\u00bb.',\n  wrIncompat: 'Un drive {m} non pu\u00f2 scrivere un\\'immagine {f}.',\n  estSim: 'Con la velocit\u00e0 istantanea del drive simulato ci vogliono pochi secondi.',\n  estReal: 'Stima con la xum1541 senza turbo: circa {m} minuti per {b} blocchi.',\n  repT: 'File gi\u00e0 presente', repB: '<p>Sul disco c\\'\u00e8 gi\u00e0 un file chiamato <b>\u00ab{n}\u00bb<\/b>. Vuoi sostituirlo?<\/p>', bRep: 'Sostituisci', bSkip: 'Salta',\n  cancel: 'Annulla', close: 'Chiudi',\n  done: 'Fatto.', saved: 'Salvato \u00ab{n}\u00bb.', written: 'Scritto \u00ab{n}\u00bb sul disco.', imgRead: 'Disco letto: {n} blocchi. L\\'immagine \u00e8 nella scheda \u00abImmagine disco\u00bb.',\n  imgReadErr: 'Disco letto con {k} blocchi illeggibili (riempiti con zeri).', imgWritten: 'Immagine scritta: {n} blocchi.',\n  fileN: '{n} file', tooBig: '\u00ab{n}\u00bb \u00e8 troppo grande per un disco ({s} byte).', badImg: '\u00ab{n}\u00bb: {e}', skipped: 'Ignorati {n} file di tipo non supportato.',\n  zipFrom: 'da {z}', fromCsdb: 'da CSDb', fromImg: 'dall\\'immagine {i}', fromFolder: 'dalla cartella {f}', fromDrive: 'dal drive',\n  c64Name: 'Nome sul C64', listInfo: '{n} file pronti, {b} blocchi', bOpen: 'Apri', bRemove: 'Rimuovi', bToDrive: 'Scrivi sul disco',\n  imgNone: 'Nessuna immagine aperta. Apri un file D64, D71 o D81 dalla scheda \u00abFile del PC\u00bb, da CSDb, oppure leggi il disco del drive con \u00abDisco \u2192 immagine\u00bb.',\n  imgHead: '{f} \u00b7 {t} tracce \u00b7 {u} file \u00b7 {fr} blocchi liberi su {tot}',\n  bImgDl: '\u2b07 Scarica l\\'immagine', bExtAll: 'Estrai tutti i file', bToSim: 'Inserisci nel drive simulato', bImgWr: 'Scrivi sul disco del drive',\n  bamT: 'Mappa dei blocchi (BAM)', lFree: 'libero', lUsed: 'occupato', lSys: 'directory e BAM', lSel: 'file selezionato', lOut: 'fuori dalla BAM',\n  tipT: 'Traccia {t}, settore {s}: ', tipFree: 'libero', tipSys: 'directory \/ BAM', tipFile: '\u00ab{n}\u00bb', tipUsed: 'occupato', tipOut: 'traccia extra',\n  vBasic: 'BASIC', vHex: 'Esadecimale', vText: 'Testo', loadAt: 'caricamento ${a}', bytes: '{n} byte', blocks: '{n} blocchi', block1: '1 blocco', showAll: 'Mostra tutto ({n} byte)',\n  splat: 'file non chiuso', locked: 'protetto',\n  simInserted: 'Immagine inserita nel drive simulato.', simIncompat: 'Il drive simulato {m} non legge immagini {f}: scegli il drive giusto.', simOnly: 'Disponibile solo con il drive simulato.',\n  extDone: 'Estratti {n} file.',\n  csNeedProxy: 'La ricerca su CSDb non \u00e8 attiva su questo sito: manca il proxy lato server (snippet WPCode).', csErr: 'CSDb non risponde ({e}). Riprova tra poco.',\n  csNone: 'Nessuna release trovata per \u00ab{q}\u00bb.', csN: '{n} release trovate', csBy: 'di {g}', csDl: '{n} download', csOpen: 'Apri', csExt: 'link esterno \u2197', csNoDl: 'Nessun file scaricabile per questa release.',\n  csSlow: 'Troppe richieste a CSDb in poco tempo: riprova tra {s} secondi.', csLoading: 'Ricerca in corso\u2026', csGetting: 'Scarico \u00ab{n}\u00bb da CSDb\u2026', csOnly: 'Si aprono solo file di csdb.dk', csPage: 'Pagina su CSDb \u2197',\n  stErr: 'Errore', copied: 'Registro copiato.',\n  driveSays: 'Il drive risponde: {s}'\n },\n en: {\n  title: 'C64 floppies in the browser',\n  intro: 'Connect a 1541, 1571 or 1581 drive to your computer with an xum1541 or ZoomFloppy and manage disks from this page: directory, format, scratch, rename, copy PRG files from your PC and from CSDb, D64 images. No hardware? Try everything with the simulated drive.',\n  connT: 'Connection', mUsb: '\u25cf Real drive', mUsbS: 'xum1541 or ZoomFloppy over USB: writes real floppies', mSim: 'Simulation', mSimS: 'no hardware: no real floppy is touched',\n  flagOn: '<b>\u25cf ONLINE<\/b> Real {m} drive connected through the xum1541: operations read and write the floppy in the drive.',\n  flagOff: '<b>\u25cb OFFLINE<\/b> No drive connected. Connect the xum1541 above, or try the simulation; D64 images can be managed anyway in the \u201cDisk image\u201d tab.',\n  flagSim: '<b>SIMULATION<\/b> Virtual disk in memory: no real floppy is read or written. To work on real disks you need the xum1541; without it you can try the commands and manage D64 images.',\n  trySim: 'Try the simulation', drvTSim: 'Simulated disk', drvTOff: 'Drive not connected',\n  bSep: 'Separator\u2026', bImgSep: '+ Separator', opSep: 'Writing the separator', sepT: 'Add a separator',\n  sepB: '<p>Adds a line like <code>0 \"----------------\" DEL<\/code> to the directory: a 0-block DEL file that takes no space. It goes in the first free directory slot, usually at the end.<\/p><p>On a real drive the directory block is written directly (<code>U1<\/code>\/<code>U2<\/code> commands): it works in Chrome and Edge, like the whole USB connection. In D64, D71 and D81 images it works in every browser.<\/p>',\n  sepStyle: 'Style', sepDash: '- - - dashes', sepLine: '\u2500\u2500\u2500 PETSCII graphic line', sepEq: '=== equals', sepStar: '*** asterisks', sepCustom: 'custom text', sepText: 'Text (only for \u201ccustom\u201d, max 16)', sepLock: 'locked \u201c<\u201d (recommended: DOS will not scratch it with S:)', bSepGo: 'Add',\n  sepDone: 'Separator added.', delSepB: '<p>Remove the separator <b>\u201c{n}\u201d<\/b>? Only its directory line is deleted.<\/p>', imgChanged: 'Image modified: download it to save it on your PC.', aRm: '\u2715 Remove',\n  usbGo: 'Connect the xum1541', usbAgain: 'Reconnect last one', usbOff: 'Disconnect',\n  usbNo: 'This browser does not support WebUSB: to connect the xum1541 you need <b>Chrome<\/b>, <b>Edge<\/b> or another Chromium browser on a computer. The simulated drive and the image viewer work everywhere.',\n  usbNoSecure: 'WebUSB only works on https pages.',\n  usbIdle: 'Plug the xum1541 (or ZoomFloppy) into a USB port, switch the drive on and press <b>Connect<\/b>: the browser will list the compatible devices.',\n  usbKnown: 'This browser already has permission for an xum1541: you can reconnect it without picking it again.',\n  usbOn: 'Connected: <b>{n}<\/b>, firmware v{v}{g}.',\n  usbCancel: 'No device selected.',\n  usbClaim: 'The browser sees the xum1541 but cannot open it. On Windows it needs the <b>WinUSB<\/b> driver (see below); on Linux a udev rule; everywhere, close other programs using it (OpenCBM, cbmctrl, VICE).',\n  usbLost: 'The xum1541 was disconnected.',\n  helpT: 'Before connecting: browser, drivers and permissions',\n  helpL: '<li><b>Browser<\/b>: Chrome, Edge, Opera or Brave on a computer. Firefox and Safari have no WebUSB.<\/li>' +\n   '<li><b>Windows<\/b>: Chrome can only use devices with the <b>WinUSB<\/b> driver. Download <a href=\"https:\/\/zadig.akeo.ie\/\" target=\"_blank\" rel=\"noopener\">Zadig<\/a>, menu <i>Options \u2192 List All Devices<\/i>, pick the xum1541 (VID <code>16D0<\/code>, PID <code>0504<\/code>), select <i>WinUSB<\/i> and press <i>Replace Driver<\/i>. If you used OpenCBM with another driver (libusb-win32 or libusbK), check that it still works.<\/li>' +\n   '<li><b>Linux<\/b>: create <code>\/etc\/udev\/rules.d\/45-xum1541.rules<\/code> with<br><code>SUBSYSTEM==\"usb\", ATTR{idVendor}==\"16d0\", ATTR{idProduct}==\"0504\", TAG+=\"uaccess\"<\/code><br>then <code>sudo udevadm control --reload-rules<\/code> and replug the xum1541.<\/li>' +\n   '<li><b>macOS<\/b>: no driver needed.<\/li>' +\n   '<li>Only one program at a time can use the xum1541: close OpenCBM, cbmctrl and similar.<\/li>' +\n   '<li>Without fast loaders the serial bus is slow: reading or writing a whole disk takes several minutes.<\/li>',\n  simModel: 'Drive', simDisk: 'Disk', dDemo: 'demo', dBlank: 'empty, formatted', dNone: 'no disk',\n  simSpeed: 'Speed', spInst: 'instant', spReal: 'realistic (serial bus)', simWp: 'write-protected disk', simDl: 'Download the simulated disk',\n  simNote: 'The simulated drive answers like a real xum1541 with a drive attached: the page uses exactly the same commands. With <b>realistic<\/b> speed the timings are an estimate of the serial bus without fast loaders.',\n  devNr: 'Device', bStatus: 'Read status', bIdent: 'Identify (UJ)', bReset: 'Bus reset',\n  drvT: 'Disk in the drive', bDir: 'Directory', bFmt: 'Format\u2026', bVal: 'Validate', bCmd: 'DOS command\u2026', bRdDisk: 'Disk \u2192 image', bWrDisk: 'Image \u2192 disk', bStop: 'Stop',\n  dropDrv: 'Drop PRG or SEQ files here to write them to the disk',\n  tFiles: 'PC files', tImg: 'Disk image', tLog: 'Log',\n  dropBig: 'Drop files from your computer here', dropSmall: 'PRG, P00, SEQ, D64, D71, D81 images and ZIP archives',\n  bPick: 'Choose files', bPickDir: 'Open a folder', bWrAll: 'Write all to disk', bClr: 'Clear list',\n  csPh: 'Search CSDb: title, or a release number or link', bSearch: 'Search',\n  csNote: 'Search and downloads go through the RetroFixer server, because CSDb does not let the browser download files directly. Only files hosted on csdb.dk can be opened.',\n  logBus: 'show bus and USB commands', bCopy: 'Copy', bClear: 'Clear',\n  credits: '<p>The xum1541 USB protocol is the one documented by the <a href=\"https:\/\/github.com\/OpenCBM\/OpenCBM\" target=\"_blank\" rel=\"noopener\">OpenCBM<\/a> project (firmware by Nate Lawson): this page is an independent JavaScript implementation using WebUSB. <a href=\"https:\/\/csdb.dk\/\" target=\"_blank\" rel=\"noopener\">CSDb<\/a> files belong to their authors.<\/p><p>Prototype: drivers, timings and whole-disk copies still need to be checked with real hardware. Copy-protected original disks cannot be duplicated with DOS commands.<\/p>',\n  stUsb: 'WebUSB: <b>available<\/b>', stUsbNo: 'WebUSB: <i>not available in this browser<\/i>', stFs: 'folders: <b>yes<\/b>', stFsNo: 'folders: read only',\n  stFmt: 'D64 \u00b7 D71 \u00b7 D81 \u00b7 PRG \u00b7 P00 \u00b7 SEQ \u00b7 ZIP',\n  noConn: 'NO DRIVE CONNECTED.', noConn2: 'Choose the simulated drive or connect an xum1541.',\n  loading: 'LOAD\"$\",{d}', searching: 'SEARCHING FOR $', ready: 'READY.',\n  subDir: '{n} files \u00b7 {f} blocks free', sub0: 'empty disk \u00b7 {f} blocks free',\n  aDl: '\u2b07 PC', aRen: '\u270e Rename', aDel: '\u2715 Scratch', aView: 'Show', aDrive: '\u2192 Drive', aExt: '\u2b07 Extract',\n  notPresent: 'No drive answers at device {0}: check that it is switched on, connected and set to the right number.', aborted: 'Operation stopped.',\n  busy: 'Wait for the current operation to finish.',\n  opDir: 'Reading the directory', opRead: 'Reading \u201c{n}\u201d', opWrite: 'Writing \u201c{n}\u201d', opFmt: 'Formatting', opVal: 'Validating', opDel: 'Scratching \u201c{n}\u201d',\n  opRen: 'Renaming', opRdDisk: 'Reading the whole disk', opWrDisk: 'Writing the image to disk', opFmtFirst: 'Formatting before writing\u2026', opId: 'Identifying the drive', opSt: 'Reading status', opRst: 'Bus reset',\n  eta: '{d} of {t} \u00b7 {r} \u00b7 {e} left', etaBlk: 'block {d} of {t} (track {tt}, sector {ss}) \u00b7 {e} left', rate: '{v} bytes\/s',\n  fmtT: 'Format the disk', fmtB: '<p class=\"warn\">Everything on the disk will be erased.<\/p><p>A full format writes every track and takes over a minute on a 1541. A quick format only clears the directory and BAM, and only works on disks that are already formatted.<\/p>',\n  fName: 'Disk name (max 16 characters)', fId: 'ID (2 characters)', fQuick: 'quick format (no ID)', bFmtGo: 'Format',\n  valT: 'Validate the disk', valB: '<p>The <code>V<\/code> command rebuilds the free block map (BAM) by reading every file, and removes files left open (the ones with an asterisk, like <code>*PRG<\/code>).<\/p><p>On disks with protections or special files it may free blocks used by custom loaders: better not to do it then.<\/p>', bValGo: 'Validate',\n  delT: 'Scratch file', delB: '<p>Scratch <b>\u201c{n}\u201d<\/b> from the disk? The command is <code>S0:{n}<\/code>.<\/p>', bDelGo: 'Scratch',\n  renT: 'Rename file', renF: 'New name', bRenGo: 'Rename',\n  cmdT: 'DOS command', cmdB: '<p>The command is sent on channel 15 and the drive\\'s answer appears below. Examples: <code>I0<\/code> initialise, <code>UJ<\/code> drive reset, <code>S0:NAME<\/code>, <code>R0:NEW=OLD<\/code>, <code>C0:COPY=ORIGINAL<\/code>.<\/p>', cmdF: 'Command', bSend: 'Send',\n  rdT: 'Disk \u2192 image', rdB: '<p>Reads every block of the disk with <code>U1<\/code> commands and creates an image you can download or open in the viewer.<\/p><p>{e}<\/p>', rdFmt: 'Format', bRdGo: 'Read the disk',\n  wrT: 'Image \u2192 disk', wrB: '<p>Writes the image <b>\u201c{n}\u201d<\/b> to the disk in the drive with <code>U2<\/code> commands.<\/p><p class=\"warn\">The current contents of the disk will be lost.<\/p><p>{e}<\/p>', wrFmt: 'format first with the image\\'s name and ID', wrUsed: 'write only used blocks (faster)', bWrGo: 'Write to disk',\n  wrNoImg: 'First open a D64, D71 or D81 image in the \u201cDisk image\u201d tab.',\n  wrIncompat: 'A {m} drive cannot write a {f} image.',\n  estSim: 'With the instant simulated drive it takes a few seconds.',\n  estReal: 'Estimate with the xum1541 without fast loader: about {m} minutes for {b} blocks.',\n  repT: 'File already there', repB: '<p>The disk already has a file called <b>\u201c{n}\u201d<\/b>. Replace it?<\/p>', bRep: 'Replace', bSkip: 'Skip',\n  cancel: 'Cancel', close: 'Close',\n  done: 'Done.', saved: 'Saved \u201c{n}\u201d.', written: 'Wrote \u201c{n}\u201d to the disk.', imgRead: 'Disk read: {n} blocks. The image is in the \u201cDisk image\u201d tab.',\n  imgReadErr: 'Disk read with {k} unreadable blocks (filled with zeros).', imgWritten: 'Image written: {n} blocks.',\n  fileN: '{n} files', tooBig: '\u201c{n}\u201d is too big for a disk ({s} bytes).', badImg: '\u201c{n}\u201d: {e}', skipped: 'Skipped {n} files of unsupported type.',\n  zipFrom: 'from {z}', fromCsdb: 'from CSDb', fromImg: 'from image {i}', fromFolder: 'from folder {f}', fromDrive: 'from the drive',\n  c64Name: 'C64 name', listInfo: '{n} files ready, {b} blocks', bOpen: 'Open', bRemove: 'Remove', bToDrive: 'Write to disk',\n  imgNone: 'No image open. Open a D64, D71 or D81 file from the \u201cPC files\u201d tab, from CSDb, or read the drive\\'s disk with \u201cDisk \u2192 image\u201d.',\n  imgHead: '{f} \u00b7 {t} tracks \u00b7 {u} files \u00b7 {fr} blocks free of {tot}',\n  bImgDl: '\u2b07 Download image', bExtAll: 'Extract all files', bToSim: 'Insert in simulated drive', bImgWr: 'Write to the drive\\'s disk',\n  bamT: 'Block map (BAM)', lFree: 'free', lUsed: 'used', lSys: 'directory and BAM', lSel: 'selected file', lOut: 'outside BAM',\n  tipT: 'Track {t}, sector {s}: ', tipFree: 'free', tipSys: 'directory \/ BAM', tipFile: '\u201c{n}\u201d', tipUsed: 'used', tipOut: 'extra track',\n  vBasic: 'BASIC', vHex: 'Hex', vText: 'Text', loadAt: 'load ${a}', bytes: '{n} bytes', blocks: '{n} blocks', block1: '1 block', showAll: 'Show all ({n} bytes)',\n  splat: 'unclosed file', locked: 'locked',\n  simInserted: 'Image inserted in the simulated drive.', simIncompat: 'The simulated {m} cannot read {f} images: pick the right drive.', simOnly: 'Only available with the simulated drive.',\n  extDone: 'Extracted {n} files.',\n  csNeedProxy: 'CSDb search is not active on this site: the server-side proxy (WPCode snippet) is missing.', csErr: 'CSDb is not answering ({e}). Try again shortly.',\n  csNone: 'No release found for \u201c{q}\u201d.', csN: '{n} releases found', csBy: 'by {g}', csDl: '{n} downloads', csOpen: 'Open', csExt: 'external link \u2197', csNoDl: 'No downloadable file for this release.',\n  csSlow: 'Too many CSDb requests in a short time: try again in {s} seconds.', csLoading: 'Searching\u2026', csGetting: 'Downloading \u201c{n}\u201d from CSDb\u2026', csOnly: 'Only csdb.dk files can be opened', csPage: 'Page on CSDb \u2197',\n  stErr: 'Error', copied: 'Log copied.',\n  driveSays: 'The drive says: {s}'\n }\n}[EN ? 'en' : 'it'];\nfunction tr(k, o) { var s = T[k] !== undefined ? T[k] : k; if (o) for (var x in o) s = s.split('{' + x + '}').join(o[x]); return s; }\nRF.msg = function (k, a) { return T[k] ? tr(k, {0: a}) : null; };\nfunction $(id) { return document.getElementById(id); }\nfunction esc(s) { return String(s).replace(\/[&<>\"]\/g, function (c) { return {'&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;'}[c]; }); }\nvar pet = RF.pet, S = pet.toStr;\nfunction h4(n) { return n.toString(16).toUpperCase().padStart(4, '0'); }\nfunction fmtDur(ms) { var s = Math.max(0, Math.round(ms \/ 1000)); return s < 60 ? s + ' s' : Math.floor(s \/ 60) + ' min ' + String(s % 60).padStart(2, '0') + ' s'; }\nfunction blk(n) { return n === 1 ? tr('block1') : tr('blocks', {n: nfmt(n)}); }\nfunction nfmt(n) { return Number(n).toLocaleString(EN ? 'en-GB' : 'it-IT'); }\n\nvar root = document.querySelector('.rffl');\nroot.querySelectorAll('[data-t]').forEach(function (e) { e.textContent = tr(e.dataset.t); });\nroot.querySelectorAll('[data-tp]').forEach(function (e) { e.placeholder = tr(e.dataset.tp); });\nroot.querySelectorAll('[data-th]').forEach(function (e) { e.innerHTML = tr(e.dataset.th); });\n$('rfflDrvT').textContent = tr('drvT');\n\nvar LOCAL = \/^(localhost|127\\.)\/.test(location.hostname) || location.protocol === 'file:';\nvar CSDB = LOCAL ? '\/csdb-proxy' : '\/wp-json\/retrofixer\/v1\/csdb';\nvar HAS_FS = typeof window.showDirectoryPicker === 'function';\nvar EXT_OK = \/\\.(prg|p00|seq|s00|usr|u00|d64|d71|d81|zip)$\/i;\n\nvar st = {mode: 'sim', conn: null, busy: false, dir: null, dirSel: -1, files: [], img: null, imgSel: -1, view: 'basic', logBus: true};\nvar uid = 0;\n\n\/* ---------- registro ---------- *\/\nvar logEl = $('rfflLog'), logLines = [];\nfunction log(kind, msg) {\n  var d = new Date(), t = d.toTimeString().slice(0, 8) + '.' + String(d.getMilliseconds()).padStart(3, '0');\n  logLines.push(t + ' ' + kind.padEnd(4) + ' ' + msg);\n  if (logLines.length > 3000) logLines.splice(0, 1000);\n  var div = document.createElement('div');\n  div.className = 'k-' + kind;\n  div.innerHTML = '<span class=\"t\">' + t + '<\/span> ' + esc(msg);\n  logEl.appendChild(div);\n  while (logEl.childNodes.length > 800) logEl.removeChild(logEl.firstChild);\n  if (!$('rfflPLog').hidden) logEl.scrollTop = logEl.scrollHeight;\n}\n\n\/* ---------- avvisi ---------- *\/\nvar toastT;\nfunction toast(msg, err) {\n  var el = root.querySelector('.toast');\n  if (!el) { el = document.createElement('div'); el.className = 'toast'; el.setAttribute('role', 'status'); root.appendChild(el); }\n  el.className = 'toast' + (err ? ' err' : ''); el.textContent = msg; el.hidden = false;\n  clearTimeout(toastT); toastT = setTimeout(function () { el.hidden = true; }, err ? 7000 : 3500);\n  if (err) log('err', msg);\n}\nfunction errMsg(e) { return e && e.status ? tr('driveSays', {s: e.status.text}) : (e && e.message) || String(e); }\n\n\/* ---------- finestre di dialogo ---------- *\/\nvar dlg = $('rfflDlg');\nfunction ask(o) {\n  return new Promise(function (resolve) {\n    var f = '<form method=\"dialog\"><h3>' + esc(o.title) + '<\/h3><div class=\"body\">' + (o.body || '') + '<\/div>';\n    if (o.fields && o.fields.length) {\n      f += '<div class=\"fields\">';\n      o.fields.forEach(function (x) {\n        if (x.type === 'check') f += '<label class=\"ck\"><input type=\"checkbox\" name=\"' + x.id + '\"' + (x.value ? ' checked' : '') + '><span>' + esc(x.label) + '<\/span><\/label>';\n        else if (x.type === 'select') f += '<label>' + esc(x.label) + '<select name=\"' + x.id + '\">' + x.options.map(function (op) { return '<option value=\"' + op[0] + '\"' + (op[0] === x.value ? ' selected' : '') + '>' + esc(op[1]) + '<\/option>'; }).join('') + '<\/select><\/label>';\n        else f += '<label>' + esc(x.label) + '<input type=\"text\" name=\"' + x.id + '\" value=\"' + esc(x.value || '') + '\" maxlength=\"' + (x.max || 40) + '\" autocomplete=\"off\" spellcheck=\"false\"' + (x.free ? ' class=\"free\"' : '') + '><\/label>';\n      });\n      f += '<\/div>';\n    }\n    \/\/ il primo pulsante nel DOM \u00e8 quello che parte con Invio: i pulsanti sono in ordine inverso e la CSS li rimette a posto\n    f += '<div class=\"out\" id=\"rfflDlgOut\"><\/div><div class=\"btns\">';\n    (o.buttons || [{v: 'ok', label: o.ok || 'OK', cls: o.danger ? 'warn' : 'pri'}]).slice().reverse().forEach(function (b) {\n      f += '<button class=\"btn ' + (b.cls || '') + '\" value=\"' + b.v + '\" type=\"submit\">' + esc(b.label) + '<\/button>';\n    });\n    if (o.cancel !== false) f += '<button class=\"btn\" value=\"cancel\" type=\"submit\" formnovalidate>' + esc(o.cancel || tr('cancel')) + '<\/button>';\n    f += '<\/div><\/form>';\n    dlg.innerHTML = f;\n    var form = dlg.querySelector('form');\n    function values() {\n      var v = {};\n      (o.fields || []).forEach(function (x) { var el = form.elements[x.id]; v[x.id] = x.type === 'check' ? el.checked : el.value; });\n      return v;\n    }\n    \/\/ si risponde subito al clic, senza aspettare l'evento \"close\" (che in alcune schede in background arriva in ritardo)\n    var done = false;\n    function finish(b) {\n      if (done) return;\n      done = true;\n      var v = b === 'cancel' ? null : Object.assign({_b: b}, values());\n      if (dlg.open) dlg.close();\n      resolve(v);\n    }\n    form.addEventListener('submit', function (ev) {\n      ev.preventDefault();\n      var b = ev.submitter ? ev.submitter.value : 'ok';\n      if (b !== 'cancel' && o.onSubmit) { o.onSubmit(values(), $('rfflDlgOut'), b); return; }\n      finish(b);\n    });\n    dlg.oncancel = function (ev) { ev.preventDefault(); finish('cancel'); };\n    dlg.onclose = function () { finish('cancel'); };\n    dlg.showModal();\n    var first = form.querySelector('input[type=text]');\n    if (first) { first.focus(); first.select(); }\n  });\n}\n\n\/* ---------- operazioni con avanzamento ---------- *\/\nfunction setBusy(b) {\n  st.busy = b;\n  $('rfflLed').classList.toggle('on', b);\n  updateButtons();\n}\nfunction progress(label) {\n  $('rfflProg').hidden = false; $('rfflProgL').textContent = label;\n  $('rfflBar').classList.add('ind'); $('rfflBar').firstChild.style.width = '0'; $('rfflEta').textContent = '';\n  return function (done, total, ms, t, s, phase) {\n    if (phase === 'format') { $('rfflProgL').textContent = tr('opFmtFirst'); return; }\n    $('rfflProgL').textContent = label;\n    if (!total) return;\n    $('rfflBar').classList.remove('ind');\n    $('rfflBar').firstChild.style.width = Math.min(100, done \/ total * 100).toFixed(1) + '%';\n    var left = done ? ms \/ done * (total - done) : 0;\n    if (t !== undefined) $('rfflEta').textContent = tr('etaBlk', {d: done, t: total, tt: t, ss: s, e: fmtDur(left)});\n    else $('rfflEta').textContent = tr('eta', {d: nfmt(done), t: nfmt(total), r: tr('rate', {v: ms > 300 ? nfmt(Math.round(done \/ ms * 1000)) : '\u2014'}), e: fmtDur(left)});\n  };\n}\nasync function op(label, fn) {\n  if (!st.conn) { toast(tr('noConn2'), true); return; }\n  if (st.busy) { toast(tr('busy'), true); return; }\n  setBusy(true);\n  var p = progress(label);\n  log('info', label);\n  try { return await fn(p); }\n  catch (e) { toast(errMsg(e), true); if (e && e.status) showStatus(e.status); throw e; }\n  finally { setBusy(false); $('rfflProg').hidden = true; }\n}\nfunction quiet(p) { return p.catch(function () {}); }\n\n\/* ---------- stato del drive ---------- *\/\nfunction showStatus(s) {\n  var el = $('rfflDstat');\n  el.textContent = s.text;\n  el.className = 'dstat' + (s.code >= 20 && s.code !== 73 ? ' err' : s.code < 2 ? ' ok' : '');\n}\nfunction updateButtons() {\n  var c = !!st.conn, b = st.busy;\n  ['rfflDir', 'rfflFmt', 'rfflVal', 'rfflCmd', 'rfflSep', 'rfflRdD', 'rfflSt', 'rfflId', 'rfflRst'].forEach(function (id) { $(id).disabled = !c || b; });\n  $('rfflWrD').disabled = !c || b || !st.img;\n  $('rfflWrAll').disabled = !c || b;\n  $('rfflDev').disabled = b;\n  $('rfflPwr').classList.toggle('on', c);\n  root.querySelectorAll('[data-need=drive]').forEach(function (e) { e.disabled = !c || b; });\n  $('rfflSimDl').disabled = !(st.conn && st.conn.fake && st.conn.fake.disk);\n}\n\n\/* ---------- schermo C64 ---------- *\/\nfunction renderScreen(el, d, o) {\n  o = o || {};\n  var html = '<div class=\"ln\"><span class=\"tx\">' + esc(o.cmd || tr('loading', {d: st.conn ? st.conn.drive.dev : 8})) + '<\/span><\/div>';\n  html += '<div class=\"ln\"><span class=\"tx\">' + esc(tr('searching')) + '<\/span><\/div>';\n  var nm = S(d.name), pad = (nm + '                ').slice(0, 16);\n  html += '<div class=\"ln hdr\"><span class=\"tx\">0 <span>\"' + esc(pad) + '\" ' + esc(d.idText || '') + '<\/span><\/span><\/div>';\n  d.files.forEach(function (f, i) {\n    var n = S(f.name), line = (String(f.blocks) + '    ').slice(0, Math.max(5, String(f.blocks).length + 1)) + '\"' + n + '\"' + ' '.repeat(Math.max(0, 16 - n.length)) + ' ' + (f.closed ? ' ' : '*') + f.typeName + (f.locked ? '<' : '');\n    html += '<div class=\"ln row' + (i === o.sel ? ' sel' : '') + '\" role=\"listitem\" tabindex=\"0\" data-i=\"' + i + '\"><span class=\"tx\">' + esc(line) + '<\/span>';\n    if (o.actions) {\n      html += '<span class=\"acts\">';\n      o.actions.forEach(function (a) {\n        if (a.when && !a.when(f)) return;\n        html += '<span role=\"button\" tabindex=\"0\" data-a=\"' + a.k + '\" class=\"' + (a.cls || '') + '\">' + esc(a.label) + '<\/span>';\n      });\n      html += '<\/span>';\n    }\n    html += '<\/div>';\n  });\n  html += '<div class=\"ln\"><span class=\"tx\">' + esc((d.free === null ? '?' : d.free) + ' BLOCKS FREE.') + '<\/span><\/div>';\n  html += '<div class=\"ln\"><span class=\"tx\">' + esc(tr('ready')) + '<\/span><\/div><div class=\"ln\"><span class=\"tx\"><span class=\"cur\"><\/span><\/span><\/div>';\n  el.innerHTML = html;\n  el.onclick = function (ev) {\n    var r = ev.target.closest('.row'); if (!r) return;\n    var i = +r.dataset.i, a = ev.target.closest('[data-a]');\n    if (a && o.onAct) o.onAct(a.dataset.a, d.files[i], i);\n    else if (o.onSel) o.onSel(i);\n  };\n  el.onkeydown = function (ev) {\n    if (ev.key !== 'Enter' && ev.key !== ' ') return;\n    var a = ev.target.closest('[data-a]'), r = ev.target.closest('.row');\n    if (!r) return;\n    ev.preventDefault();\n    if (a && o.onAct) o.onAct(a.dataset.a, d.files[+r.dataset.i], +r.dataset.i);\n    else if (o.onSel) o.onSel(+r.dataset.i);\n  };\n}\nfunction screenMsg(el, lines) {\n  el.innerHTML = lines.map(function (l) { return '<div class=\"ln\"><span class=\"tx\">' + esc(l) + '<\/span><\/div>'; }).join('') + '<div class=\"ln\"><span class=\"tx\"><span class=\"cur\"><\/span><\/span><\/div>';\n  el.onclick = null;\n}\n\n\/* ---------- drive: directory e operazioni sui file ---------- *\/\nfunction typeExt(tn) { return {PRG: 'prg', SEQ: 'seq', USR: 'usr'}[tn] || 'prg'; }\nfunction renderFlag() {\n  var f = $('rfflFlag'), p = $('rfflDrv'), c = st.conn, mode = c ? (c.fake ? 'sim' : 'on') : 'off';\n  p.dataset.mode = mode; f.className = 'mflag ' + mode;\n  f.innerHTML = mode === 'on' ? tr('flagOn', {m: esc(c.drive.model || '')}).replace('  ', ' ') : mode === 'sim' ? tr('flagSim') :\n    tr('flagOff') + (st.mode === 'usb' ? ' <button class=\"btn sm\" type=\"button\" data-try=\"sim\">' + esc(tr('trySim')) + '<\/button>' : '');\n  $('rfflDrvT').textContent = tr(mode === 'on' ? 'drvT' : mode === 'sim' ? 'drvTSim' : 'drvTOff');\n}\nfunction renderDrive() {\n  var scr = $('rfflScr');\n  renderFlag();\n  if (!st.conn) { screenMsg(scr, ['    **** COMMODORE 64 BASIC V2 ****', '', tr('noConn'), tr('noConn2')]); $('rfflDrvSub').textContent = ''; return; }\n  if (!st.dir) { screenMsg(scr, [tr('ready')]); return; }\n  var d = st.dir;\n  $('rfflDrvSub').textContent = (d.files.length ? tr('subDir', {n: d.files.length, f: d.free}) : tr('sub0', {f: d.free})) + (st.conn.drive.model ? ' \u00b7 ' + st.conn.drive.model : '');\n  renderScreen(scr, d, {sel: st.dirSel, actions: [\n    {k: 'dl', label: tr('aDl'), when: function (f) { return f.typeName !== 'DEL' && f.typeName !== 'REL'; }},\n    {k: 'ren', label: tr('aRen')},\n    {k: 'del', label: tr('aDel'), cls: 'del', when: function (f) { return f.typeName !== 'DEL'; }},\n    {k: 'rmsep', label: tr('aRm'), cls: 'del', when: function (f) { return f.typeName === 'DEL'; }}\n  ], onSel: function (i) { st.dirSel = st.dirSel === i ? -1 : i; renderDrive(); }, onAct: driveAct});\n}\nasync function refreshDir() {\n  var c = st.conn;\n  screenMsg($('rfflScr'), [tr('loading', {d: c.drive.dev}), '', tr('searching')]);\n  try {\n    var d = await op(tr('opDir'), function (p) { return c.drive.directory(p); });\n    d.idText = S(d.idText || d.id);\n    st.dir = d; st.dirSel = -1;\n    log('info', d.files.length + ' file, ' + d.free + ' blocks free');\n  } catch (e) { st.dir = null; }\n  renderDrive();\n}\nasync function driveAct(k, f) {\n  var c = st.conn, name = S(f.name);\n  if (k === 'dl') {\n    try {\n      var data = await op(tr('opRead', {n: name}), function (p) { return c.drive.readFile(f.name, f.typeName, p, f.blocks * 254); });\n      saveBytes(pet.toPcName(f.name, typeExt(f.typeName)), data);\n      toast(tr('saved', {n: pet.toPcName(f.name, typeExt(f.typeName))}));\n    } catch (e) {}\n  } else if (k === 'ren') {\n    var r = await ask({title: tr('renT'), fields: [{id: 'n', label: tr('renF'), value: name, max: 16}], ok: tr('bRenGo')});\n    if (!r || !r.n.trim()) return;\n    var nb = pet.fromStr(r.n).slice(0, 16);\n    try { var s = await op(tr('opRen'), function () { return c.drive.rename(f.name, nb); }); showStatus(s); await refreshDir(); } catch (e) {}\n  } else if (k === 'rmsep') {\n    if (!(await ask({title: tr('delT'), body: tr('delSepB', {n: esc(name)}), ok: tr('aRm'), danger: true}))) return;\n    try { var s3 = await op(tr('opDel', {n: name}), function () { return c.drive.removeDel(f.name); }); showStatus(s3); await refreshDir(); } catch (e) {}\n  } else if (k === 'del') {\n    var ok = await ask({title: tr('delT'), body: tr('delB', {n: esc(name)}), ok: tr('bDelGo'), danger: true});\n    if (!ok) return;\n    try { var s2 = await op(tr('opDel', {n: name}), function () { return c.drive.scratch(f.name); }); showStatus(s2); await refreshDir(); } catch (e) {}\n  }\n}\nvar SEP = {dash: 0x2D, line: 0xC0, eq: 0x3D, star: 0x2A};\nasync function askSeparator() {\n  var r = await ask({title: tr('sepT'), body: tr('sepB'), ok: tr('bSepGo'), fields: [\n    {id: 'y', type: 'select', label: tr('sepStyle'), value: 'dash', options: [['dash', tr('sepDash')], ['line', tr('sepLine')], ['eq', tr('sepEq')], ['star', tr('sepStar')], ['custom', tr('sepCustom')]]},\n    {id: 't', label: tr('sepText'), value: '', max: 16},\n    {id: 'l', type: 'check', label: tr('sepLock'), value: true}]});\n  if (!r) return null;\n  var name = r.y === 'custom' ? pet.fromStr(r.t).slice(0, 16) : new Array(16).fill(SEP[r.y]);\n  return name.length ? {name: name, locked: r.l} : null;\n}\nasync function driveSeparator() {\n  var c = st.conn, x = await askSeparator();\n  if (!x) return;\n  try { var s = await op(tr('opSep'), function () { return c.drive.addSeparator(x.name, x.locked); }); showStatus(s); toast(tr('sepDone')); } catch (e) {}\n  await refreshDir();\n}\nfunction estimate(blocks) {\n  if (st.conn && st.conn.fake && st.conn.fake.speed !== 'real') return tr('estSim');\n  return tr('estReal', {m: Math.max(1, Math.round(blocks * 0.8 \/ 60)), b: blocks});\n}\n\n\/* ---------- collegamento ---------- *\/\nfunction setMode(m) {\n  st.mode = m;\n  root.querySelectorAll('.modes button').forEach(function (b) { b.setAttribute('aria-selected', b.dataset.m === m ? 'true' : 'false'); });\n  $('rfflUsbBox').hidden = m !== 'usb'; $('rfflSimBox').hidden = m !== 'sim';\n  try { localStorage.setItem('rfflMode', m); } catch (e) {}\n}\nasync function disconnect() {\n  var c = st.conn;\n  st.conn = null; st.dir = null;\n  if (c) { try { await c.xum.close(); } catch (e) {} log('info', 'disconnesso'); }\n  $('rfflDstat').textContent = '\u2014'; $('rfflDstat').className = 'dstat';\n  $('rfflUsbOff').hidden = true;\n  updateButtons(); renderDrive();\n}\nasync function attach(xum, fake) {\n  await xum.open();\n  var cbm = new RF.Cbm(xum, log), drive = new RF.Drive(cbm, +$('rfflDev').value);\n  st.conn = {xum: xum, cbm: cbm, drive: drive, fake: fake || null};\n  updateButtons();\n  var s = await op(tr('opSt'), function () { return drive.status(); });\n  showStatus(s);\n  if (s.none) { toast(tr('notPresent', {0: drive.dev}), true); renderDrive(); return; }\n  drive.setModel(s);\n  await refreshDir();\n}\nfunction simDisk(model, kind) {\n  var f = RF.DOS_MODELS[model].fmt;\n  if (kind === 'none') return null;\n  if (kind === 'blank') return RF.Disk.blank(f, pet.fromStr('DISCO VUOTO'), pet.fromStr('01'));\n  return RF.demoDisk(f);\n}\nasync function connectSim() {\n  if (st.busy) return;\n  await disconnect();\n  var model = $('rfflSimModel').value;\n  var fake = new RF.FakeDrive({model: model, dev: +$('rfflDev').value, speed: $('rfflSimSpeed').value, writeProtect: $('rfflSimWp').checked,\n    disk: simDisk(model, $('rfflSimDisk').value), onChange: updateButtons, onLed: function (on) { $('rfflLed').classList.toggle('on', on || st.busy); }});\n  log('info', 'drive simulato ' + model + ' al numero ' + fake.dev);\n  try { await attach(new RF.Xum1541(new RF.FakeXum1541(fake), log), fake); } catch (e) { toast(errMsg(e), true); }\n}\nasync function connectUsb(device) {\n  if (st.busy) return;\n  var note = $('rfflUsbNote');\n  try {\n    if (!device) device = await RF.Xum1541.request();\n  } catch (e) {\n    note.innerHTML = e && e.name === 'NotFoundError' ? tr('usbCancel') : esc(errMsg(e)); note.className = 'note' + (e && e.name === 'NotFoundError' ? '' : ' err');\n    return;\n  }\n  await disconnect();\n  var xum = new RF.Xum1541(device, log);\n  try {\n    await attach(xum, null);\n    note.innerHTML = tr('usbOn', {n: esc(device.productName || 'xum1541'), v: xum.version, g: xum.gitrev ? ' (' + esc(xum.gitrev) + ')' : ''}); note.className = 'note';\n    $('rfflUsbOff').hidden = false; $('rfflUsbAgain').hidden = true;\n  } catch (e) {\n    log('err', (e && e.name ? e.name + ': ' : '') + errMsg(e));\n    var claim = e && (e.name === 'NetworkError' || e.name === 'SecurityError' || \/claim|access|denied|open\/i.test(e.message || ''));\n    note.innerHTML = claim ? tr('usbClaim') : esc(errMsg(e)); note.className = 'note err';\n    try { await xum.close(); } catch (x) {}\n    st.conn = null; updateButtons(); renderDrive();\n  }\n}\nfunction usbInit() {\n  var note = $('rfflUsbNote');\n  if (!RF.Xum1541.supported()) {\n    note.innerHTML = window.isSecureContext === false ? tr('usbNoSecure') : tr('usbNo'); note.className = 'note err';\n    $('rfflUsbGo').disabled = true; return;\n  }\n  note.innerHTML = tr('usbIdle');\n  navigator.usb.addEventListener('disconnect', function (ev) {\n    if (st.conn && st.conn.xum.dev === ev.device) { toast(tr('usbLost'), true); note.innerHTML = tr('usbLost'); st.busy = false; disconnect(); }\n  });\n  RF.Xum1541.known().then(function (l) {\n    if (!l.length) return;\n    st.knownUsb = l[0];\n    $('rfflUsbAgain').hidden = false; note.innerHTML = tr('usbKnown');\n  });\n}\n\n\/* ---------- file del PC ---------- *\/\nfunction saveBytes(name, bytes) {\n  var a = document.createElement('a'), url = URL.createObjectURL(new Blob([bytes], {type: 'application\/octet-stream'}));\n  a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove();\n  setTimeout(function () { URL.revokeObjectURL(url); }, 4000);\n}\nvar MAXFILE = 664 * 254 * 5;\nasync function addEntries(list, from, autoOpen) {\n  var added = [], skipped = 0;\n  for (var i = 0; i < list.length; i++) {\n    var e = list[i], nm = e.name, low = nm.toLowerCase(), data = e.data;\n    try {\n      if (\/\\.zip$\/.test(low)) {\n        var z = await RF.readZip(data);\n        var inner = z.filter(function (x) { return EXT_OK.test(x.name) && !\/\\.zip$\/i.test(x.name); });\n        skipped += z.length - inner.length;\n        added = added.concat(await addEntries(inner, tr('zipFrom', {z: nm}), false));\n        continue;\n      }\n      if (\/\\.(d64|d71|d81)$\/.test(low) || RF.Disk.detect(data.length) && !\/\\.(prg|seq|usr|p00|s00|u00)$\/.test(low)) {\n        var disk = new RF.Disk(data, nm);\n        var it = {id: ++uid, kind: 'img', pc: nm, size: data.length, disk: disk, from: from};\n        st.files.push(it); added.push(it); continue;\n      }\n      if (!EXT_OK.test(low)) { skipped++; continue; }\n      var type = \/\\.(seq|s00)$\/.test(low) ? 'SEQ' : \/\\.(usr|u00)$\/.test(low) ? 'USR' : 'PRG', c64 = null;\n      if (\/\\.[psu]00$\/.test(low)) { var p0 = RF.parseP00(data); if (p0) { c64 = p0.name; data = p0.data; } }\n      if (data.length > MAXFILE) { toast(tr('tooBig', {n: nm, s: nfmt(data.length)}), true); continue; }\n      var f = {id: ++uid, kind: 'file', pc: nm, size: data.length, data: data, c64: c64 || pet.fromPcName(nm), type: type, from: from};\n      st.files.push(f); added.push(f);\n    } catch (err) { toast(tr('badImg', {n: nm, e: err.message}), true); }\n  }\n  if (skipped) toast(tr('skipped', {n: skipped}));\n  if (autoOpen !== false) {\n    renderFiles();\n    var imgs = added.filter(function (x) { return x.kind === 'img'; });\n    if (imgs.length === 1 && added.length === 1) { openImage(imgs[0].disk, imgs[0].pc); showTab('img'); }\n    else showTab('files');\n  }\n  return added;\n}\nasync function readFiles(fileList, from) {\n  var arr = [];\n  for (var i = 0; i < fileList.length; i++) {\n    var f = fileList[i];\n    if (f.size > 16 * 1024 * 1024) continue;\n    arr.push({name: f.name, data: new Uint8Array(await f.arrayBuffer())});\n  }\n  return addEntries(arr, from);\n}\nasync function pickFolder() {\n  if (!HAS_FS) { $('rfflInpDir').click(); return; }\n  var dir;\n  try { dir = await window.showDirectoryPicker({mode: 'read'}); } catch (e) { return; }\n  var out = [];\n  async function walk(h, depth) {\n    for await (var entry of h.values()) {\n      if (out.length >= 400) return;\n      if (entry.kind === 'file' && EXT_OK.test(entry.name)) out.push(await entry.getFile());\n      else if (entry.kind === 'directory' && depth < 3) await walk(entry, depth + 1);\n    }\n  }\n  await walk(dir, 0);\n  await readFiles(out, tr('fromFolder', {f: dir.name}));\n}\nfunction blocksOf(n) { return Math.max(1, Math.ceil(n \/ 254)); }\nfunction renderFiles() {\n  var el = $('rfflList'), files = st.files;\n  $('rfflNFiles').textContent = files.length;\n  $('rfflListBar').hidden = !files.length;\n  var nf = files.filter(function (f) { return f.kind === 'file'; });\n  $('rfflListInfo').textContent = tr('listInfo', {n: nf.length, b: nf.reduce(function (a, f) { return a + blocksOf(f.data.length); }, 0)});\n  el.innerHTML = files.map(function (f) {\n    var h = '<div class=\"fitem\" data-id=\"' + f.id + '\">';\n    if (f.kind === 'img') {\n      h += '<span class=\"ty img\">' + f.disk.fmt.label + '<\/span><div class=\"nm\"><b title=\"' + esc(f.pc) + '\">' + esc(f.pc) + '<\/b><small>\u00ab' + esc(S(f.disk.headerInfo().name)) + '\u00bb \u00b7 ' + tr('fileN', {n: f.disk.files().length}) + ' \u00b7 ' + nfmt(f.size) + ' byte' + (f.from ? ' \u00b7 ' + esc(f.from) : '') + '<\/small><\/div>';\n      h += '<div class=\"fa\"><button class=\"btn sm pri\" data-a=\"open\" type=\"button\">' + esc(tr('bOpen')) + '<\/button><button class=\"btn sm\" data-a=\"rm\" type=\"button\" aria-label=\"' + esc(tr('bRemove')) + '\">\u2715<\/button><\/div>';\n    } else {\n      h += '<span class=\"ty' + (f.type === 'SEQ' ? ' seq' : '') + '\">' + f.type + '<\/span><div class=\"nm\"><b title=\"' + esc(f.pc) + '\">' + esc(f.pc) + '<\/b><small>' + nfmt(f.size) + ' byte \u00b7 ' + blk(blocksOf(f.size)) + (f.type === 'PRG' && f.size > 1 ? ' \u00b7 ' + tr('loadAt', {a: h4(f.data[0] | (f.data[1] << 8))}) : '') + (f.from ? ' \u00b7 ' + esc(f.from) : '') + '<\/small><\/div>';\n      h += '<div class=\"fa\"><button class=\"btn sm pri\" data-a=\"wr\" data-need=\"drive\" type=\"button\">' + esc(tr('bToDrive')) + '<\/button><button class=\"btn sm\" data-a=\"rm\" type=\"button\" aria-label=\"' + esc(tr('bRemove')) + '\">\u2715<\/button><\/div>';\n      h += '<div class=\"c64\"><span>' + esc(tr('c64Name')) + '<\/span><input type=\"text\" maxlength=\"16\" spellcheck=\"false\" value=\"' + esc(S(f.c64)) + '\" data-a=\"nm\"><select data-a=\"ty\">' + ['PRG', 'SEQ', 'USR'].map(function (t) { return '<option' + (t === f.type ? ' selected' : '') + '>' + t + '<\/option>'; }).join('') + '<\/select><\/div>';\n    }\n    return h + '<\/div>';\n  }).join('');\n  updateButtons();\n}\nfunction fileById(id) { for (var i = 0; i < st.files.length; i++) if (st.files[i].id === id) return st.files[i]; return null; }\nasync function writeToDrive(f, all) {\n  var c = st.conn, name = S(f.c64), replace = false;\n  if (!c) { toast(tr('noConn2'), true); return false; }\n  if (st.dir && st.dir.files.some(function (x) { return pet.eq(x.name, f.c64); })) {\n    var r = await ask({title: tr('repT'), body: tr('repB', {n: esc(name)}), buttons: [{v: 'skip', label: tr('bSkip')}, {v: 'rep', label: tr('bRep'), cls: 'warn'}]});\n    if (!r || r._b !== 'rep') return false;\n    replace = true;\n  }\n  try {\n    var s = await op(tr('opWrite', {n: name}), function (p) { return c.drive.writeFile(f.c64, f.data, f.type, {replace: replace}, p); });\n    showStatus(s); toast(tr('written', {n: name}));\n    if (!all) await refreshDir();\n    return true;\n  } catch (e) { return false; }\n}\nasync function writeAll(list) {\n  for (var i = 0; i < list.length; i++) { if (!(await writeToDrive(list[i], true)) && !st.conn) break; }\n  await refreshDir();\n}\n\n\/* ---------- visualizzatore di immagini ---------- *\/\nfunction openImage(disk, name) { st.img = {disk: disk, name: name || 'disco.d64'}; st.imgSel = -1; renderImage(); updateButtons(); }\nfunction imgDir(disk) {\n  var h = disk.headerInfo();\n  return {name: h.name, idText: S(h.raw.slice(18, 23)).replace(\/\\s+$\/, ''), files: disk.files(), free: disk.blocksFree()};\n}\nfunction renderImage() {\n  var pane = $('rfflPImg');\n  if (!st.img) { pane.innerHTML = '<p class=\"none\">' + esc(tr('imgNone')) + '<\/p>'; return; }\n  var disk = st.img.disk, d = imgDir(disk);\n  var h = '<div class=\"vhead\"><b>' + esc(st.img.name) + '<\/b><span>' + esc(tr('imgHead', {f: disk.fmt.label, t: disk.tracks, u: d.files.length, fr: d.free, tot: disk.blocksTotal()})) + '<\/span><\/div>';\n  h += '<div class=\"vtools\"><button class=\"btn sm\" data-v=\"dl\" type=\"button\">' + esc(tr('bImgDl')) + '<\/button><button class=\"btn sm\" data-v=\"ext\" type=\"button\">' + esc(tr('bExtAll')) + '<\/button>';\n  h += '<button class=\"btn sm\" data-v=\"sim\" type=\"button\"' + (st.conn && st.conn.fake ? '' : ' disabled title=\"' + esc(tr('simOnly')) + '\"') + '>' + esc(tr('bToSim')) + '<\/button>';\n  h += '<button class=\"btn sm\" data-v=\"sep\" type=\"button\">' + esc(tr('bImgSep')) + '<\/button>';\n  h += '<button class=\"btn sm warn\" data-v=\"wr\" data-need=\"drive\" type=\"button\">' + esc(tr('bImgWr')) + '<\/button><\/div>';\n  h += '<div class=\"scr\" id=\"rfflVScr\"><\/div>';\n  h += '<div class=\"detail\" id=\"rfflDet\" hidden><\/div>';\n  h += '<div class=\"bam\"><h4>' + esc(tr('bamT')) + '<\/h4><canvas id=\"rfflBam\"><\/canvas><div class=\"tip\" id=\"rfflTip\"><\/div><div class=\"leg\">' +\n    [['#2a2366', 'lFree'], ['#7c70da', 'lUsed'], ['#ffd23f', 'lSys'], ['#3fd8ff', 'lSel'], ['#0b0920', 'lOut']].map(function (x) { return '<span><i style=\"background:' + x[0] + '\"><\/i>' + esc(tr(x[1])) + '<\/span>'; }).join('') + '<\/div><\/div>';\n  pane.innerHTML = h;\n  renderScreen($('rfflVScr'), d, {cmd: 'LOAD\"$\",8   ; ' + st.img.name, sel: st.imgSel, actions: [\n    {k: 'ext', label: tr('aExt'), when: function (f) { return f.type !== 0; }},\n    {k: 'drv', label: tr('aDrive'), when: function (f) { return f.type >= 1 && f.type <= 3; }},\n    {k: 'rm', label: tr('aRm'), cls: 'del', when: function (f) { return f.type === 0; }}\n  ], onSel: function (i) { st.imgSel = st.imgSel === i ? -1 : i; renderImage(); }, onAct: imgAct});\n  drawBam();\n  if (st.imgSel >= 0) renderDetail(d.files[st.imgSel]);\n  updateButtons();\n}\nfunction imgAct(k, e) {\n  var disk = st.img.disk, data;\n  if (k === 'rm') { disk.removeEntry(e); st.imgSel = -1; renderImage(); renderFiles(); toast(tr('imgChanged')); return; }\n  try { data = disk.readFile(e); } catch (err) { toast(err.message, true); return; }\n  if (k === 'ext') { var n = pet.toPcName(e.name, typeExt(e.typeName)); saveBytes(n, data); toast(tr('saved', {n: n})); }\n  if (k === 'drv') writeToDrive({c64: e.name, data: data, type: e.typeName});\n}\nvar bamOwner = null;\nfunction drawBam() {\n  var cv = $('rfflBam'); if (!cv) return;\n  var disk = st.img.disk, map = disk.bamMap(), maxS = 0, cell = disk.tracks > 40 ? 9 : 14;\n  map.forEach(function (r) { maxS = Math.max(maxS, r.length); });\n  cv.width = disk.tracks * cell; cv.height = maxS * cell;\n  var g = cv.getContext('2d'), files = disk.files(), sel = st.imgSel >= 0 ? files[st.imgSel] : null, selSet = {};\n  bamOwner = {};\n  files.forEach(function (f, i) {\n    if (!f.t) return;\n    try { disk.chain(f.t, f.s).forEach(function (x) { bamOwner[x[0] + ':' + x[1]] = i; if (f === sel) selSet[x[0] + ':' + x[1]] = 1; }); } catch (e) {}\n  });\n  g.fillStyle = '#0f0c24'; g.fillRect(0, 0, cv.width, cv.height);\n  for (var t = 0; t < map.length; t++) for (var s = 0; s < map[t].length; s++) {\n    var v = map[t][s], k = (t + 1) + ':' + s;\n    g.fillStyle = selSet[k] ? '#3fd8ff' : v === 0 ? (bamOwner[k] !== undefined ? '#e0398a' : '#2a2366') : v === 1 ? '#7c70da' : v === 2 ? '#ffd23f' : '#0b0920';\n    g.fillRect(t * cell + 1, s * cell + 1, cell - 2, cell - 2);\n  }\n  cv.onmousemove = function (ev) {\n    var r = cv.getBoundingClientRect(), tt = Math.floor((ev.clientX - r.left) \/ r.width * disk.tracks) + 1, ss = Math.floor((ev.clientY - r.top) \/ r.height * maxS);\n    if (tt < 1 || tt > disk.tracks || ss < 0 || ss >= disk.spt(tt)) { $('rfflTip').textContent = ''; return; }\n    var v = map[tt - 1][ss], o = bamOwner[tt + ':' + ss];\n    $('rfflTip').textContent = tr('tipT', {t: tt, s: ss}) + (o !== undefined ? tr('tipFile', {n: S(files[o].name)}) : v === 0 ? tr('tipFree') : v === 2 ? tr('tipSys') : v === 3 ? tr('tipOut') : tr('tipUsed'));\n  };\n  cv.onclick = function () {\n    var txt = $('rfflTip').textContent; if (!txt) return;\n    var m = \/(\\d+)\\D+(\\d+)\/.exec(txt), o = m && bamOwner[m[1] + ':' + m[2]];\n    if (o !== undefined && o !== null) { st.imgSel = o; renderImage(); }\n  };\n  cv.onmouseleave = function () { $('rfflTip').textContent = ''; };\n}\nfunction hexDump(data, max) {\n  var out = [], n = Math.min(data.length, max);\n  for (var i = 0; i < n; i += 16) {\n    var hx = '', as = '';\n    for (var j = 0; j < 16; j++) {\n      if (i + j < n) { var b = data[i + j]; hx += b.toString(16).padStart(2, '0') + ' '; as += b >= 0x20 && b < 0x7F ? String.fromCharCode(b) : '.'; }\n      else hx += '   ';\n      if (j === 7) hx += ' ';\n    }\n    out.push('<i>' + h4(i) + '<\/i>  ' + hx + ' <u>' + esc(as) + '<\/u>');\n  }\n  return out.join('\\n');\n}\nfunction renderDetail(e) {\n  var el = $('rfflDet'), disk = st.img.disk, data;\n  try { data = disk.readFile(e); } catch (err) { el.hidden = false; el.textContent = err.message; return; }\n  var load = data.length > 1 ? data[0] | (data[1] << 8) : 0, basic = e.type === 2 && load >= 0x0401 && load <= 0x1C01 ? RF.basicList(data) : null;\n  var views = [];\n  if (basic) views.push(['basic', tr('vBasic')]);\n  if (e.type === 1 || e.type === 3) views.push(['text', tr('vText')]);\n  views.push(['hex', tr('vHex')]);\n  var view = views.some(function (v) { return v[0] === st.view; }) ? st.view : views[0][0];\n  var h = '<div class=\"dh\"><b>\"' + esc(S(e.name)) + '\"<\/b><span>' + e.typeName + ' \u00b7 ' + blk(e.blocks) + ' \u00b7 ' + tr('bytes', {n: nfmt(data.length)}) +\n    (e.type === 2 && data.length > 1 ? ' \u00b7 ' + tr('loadAt', {a: h4(load)}) : '') + (e.closed ? '' : ' \u00b7 ' + tr('splat')) + (e.locked ? ' \u00b7 ' + tr('locked') : '') + '<\/span><span class=\"sp\"><\/span>';\n  h += '<span class=\"seg\">' + views.map(function (v) { return '<button type=\"button\" data-view=\"' + v[0] + '\" aria-pressed=\"' + (v[0] === view) + '\">' + v[1] + '<\/button>'; }).join('') + '<\/span><\/div>';\n  h += '<div class=\"vtools\"><button class=\"btn sm pri\" data-d=\"ext\" type=\"button\">' + esc(tr('aExt')) + '<\/button>';\n  if (e.type >= 1 && e.type <= 3) h += '<button class=\"btn sm\" data-d=\"drv\" data-need=\"drive\" type=\"button\">' + esc(tr('aDrive')) + '<\/button>';\n  h += '<\/div>';\n  if (view === 'basic') h += '<pre class=\"code\">' + esc(basic.lines.join('\\n')) + (basic.end < data.length - 2 ? '\\n\\n; + ' + nfmt(data.length - basic.end) + ' byte' : '') + '<\/pre>';\n  else if (view === 'text') h += '<pre class=\"code\">' + esc(S(Array.prototype.map.call(data, function (b) { return b === 13 ? 10 : b; })).replace(\/\u00b7\/g, '').split(String.fromCharCode(10)).join('\\n')) + '<\/pre>';\n  else { var lim = el.dataset.all === '1' ? data.length : 4096; h += '<pre class=\"hex\">' + hexDump(data, lim) + '<\/pre>'; if (data.length > lim) h += '<p><button class=\"btn sm\" data-d=\"all\" type=\"button\">' + esc(tr('showAll', {n: nfmt(data.length)})) + '<\/button><\/p>'; }\n  el.innerHTML = h; el.hidden = false;\n  el.onclick = function (ev) {\n    var v = ev.target.closest('[data-view]'), d = ev.target.closest('[data-d]');\n    if (v) { st.view = v.dataset.view; renderDetail(e); }\n    if (d && d.dataset.d === 'all') { el.dataset.all = '1'; renderDetail(e); }\n    if (d && (d.dataset.d === 'ext' || d.dataset.d === 'drv')) imgAct(d.dataset.d, e);\n  };\n  el.dataset.all = '';\n  updateButtons();\n}\nasync function extractAll() {\n  var disk = st.img.disk, files = disk.files().filter(function (f) { return f.type >= 1 && f.type <= 3; }), n = 0;\n  if (HAS_FS) {\n    var dir;\n    try { dir = await window.showDirectoryPicker({mode: 'readwrite'}); } catch (e) { return; }\n    var used = {};\n    for (var i = 0; i < files.length; i++) {\n      var nm = pet.toPcName(files[i].name, typeExt(files[i].typeName)), base = nm, k = 2;\n      while (used[nm.toLowerCase()]) nm = base.replace(\/(\\.\\w+)$\/, ' (' + (k++) + ')$1');\n      used[nm.toLowerCase()] = 1;\n      try {\n        var fh = await dir.getFileHandle(nm, {create: true}), w = await fh.createWritable();\n        await w.write(disk.readFile(files[i])); await w.close(); n++;\n      } catch (e) { toast(nm + ': ' + e.message, true); }\n    }\n  } else {\n    for (var j = 0; j < files.length; j++) {\n      try { saveBytes(pet.toPcName(files[j].name, typeExt(files[j].typeName)), disk.readFile(files[j])); n++; } catch (e) {}\n      await RF.util.sleep(350);\n    }\n  }\n  toast(tr('extDone', {n: n}));\n}\nasync function imgTool(k) {\n  var disk = st.img.disk;\n  if (k === 'dl') saveBytes(st.img.name.replace(\/\\.(d64|d71|d81)$\/i, '') + '.' + disk.fmt.id, disk.data);\n  else if (k === 'ext') extractAll();\n  else if (k === 'sep') {\n    var x = await askSeparator();\n    if (!x) return;\n    try { disk.addSeparator(x.name, x.locked); } catch (e) { toast(e.message, true); return; }\n    renderImage(); renderFiles(); toast(tr('imgChanged'));\n  }\n  else if (k === 'sim') {\n    var fake = st.conn && st.conn.fake;\n    if (!fake) { toast(tr('simOnly'), true); return; }\n    var ok = RF.DOS_MODELS[fake.model].fmt === disk.fmt.id || (fake.model === '1571' && disk.fmt.id === 'd64');\n    if (!ok) { toast(tr('simIncompat', {m: fake.model, f: disk.fmt.label}), true); return; }\n    fake.insert(new RF.Disk(disk.data.slice(), st.img.name));\n    toast(tr('simInserted')); await refreshDir();\n  } else if (k === 'wr') writeDiskDlg();\n}\nasync function writeDiskDlg() {\n  var c = st.conn;\n  if (!st.img) { toast(tr('wrNoImg'), true); return; }\n  var disk = st.img.disk, g = c.drive.model ? c.drive.geometry() : disk.fmt.id;\n  if (c.drive.model && disk.fmt.id !== g && !(g === 'd71' && disk.fmt.id === 'd64')) { toast(tr('wrIncompat', {m: c.drive.model, f: disk.fmt.label}), true); return; }\n  var used = disk.blocksTotal() - disk.blocksFree() + disk.fmt.spt(disk.fmt.dirTrack);\n  var r = await ask({title: tr('wrT'), body: tr('wrB', {n: esc(st.img.name), e: esc(estimate(used))}), fields: [{id: 'f', type: 'check', label: tr('wrFmt'), value: true}, {id: 'u', type: 'check', label: tr('wrUsed'), value: true}], ok: tr('bWrGo'), danger: true});\n  if (!r) return;\n  try {\n    var n = await op(tr('opWrDisk'), function (p) { return c.drive.writeDisk(disk, {format: r.f, usedOnly: r.u}, p); });\n    toast(tr('imgWritten', {n: n}));\n  } catch (e) {}\n  await refreshDir();\n}\nasync function readDiskDlg() {\n  var c = st.conn, g = c.drive.geometry();\n  var opts = [['d64', 'D64 (1541, 35 tracce)'], ['d71', 'D71 (1571, 70 tracce)'], ['d81', 'D81 (1581, 80 tracce)']];\n  var r = await ask({title: tr('rdT'), body: tr('rdB', {e: esc(estimate(RF.Disk.blank(g).sectors))}), fields: [{id: 'g', type: 'select', label: tr('rdFmt'), value: g, options: opts}], ok: tr('bRdGo')});\n  if (!r) return;\n  try {\n    var img = await op(tr('opRdDisk'), function (p) { return c.drive.readDisk(r.g, p); });\n    var h = img.headerInfo(), name = (pet.toPcName(h.name) || 'disco') + '.' + r.g;\n    openImage(img, name); showTab('img');\n    toast(img.readErrors.length ? tr('imgReadErr', {k: img.readErrors.length}) : tr('imgRead', {n: img.sectors}), img.readErrors.length > 0);\n  } catch (e) {}\n}\n\n\/* ---------- CSDb ---------- *\/\nasync function csFetch(params) {\n  var r = await fetch(CSDB + '?' + new URLSearchParams(params).toString(), {credentials: 'omit', headers: {'X-RF-Floppy': '1'}});\n  if (r.status === 404) { var e = new Error(tr('csNeedProxy')); e.proxy = true; throw e; }\n  if (r.status === 429) { var w = +r.headers.get('Retry-After') || 60; throw new Error(tr('csSlow', {s: w})); }\n  if (!r.ok) { var t = ''; try { t = (await r.json()).message || ''; } catch (x) {} throw new Error(tr('csErr', {e: t || r.status})); }\n  return r;\n}\nasync function csSearch(q) {\n  var out = $('rfflCsOut'), m = \/(?:release\\\/\\?id=|^)(\\d+)\\s*$\/.exec(q.trim());\n  if (m) return csRelease(m[1]);\n  out.innerHTML = '<p class=\"none\">' + esc(tr('csLoading')) + '<\/p>';\n  try {\n    var j = await (await csFetch({a: 'search', q: q})).json();\n    if (j.release) return csRelease(j.release);\n    if (!j.results || !j.results.length) { out.innerHTML = '<p class=\"none\">' + esc(tr('csNone', {q: q})) + '<\/p>'; return; }\n    out.innerHTML = '<p class=\"csnote\">' + esc(tr('csN', {n: j.results.length})) + '<\/p><div class=\"csres\">' + j.results.map(function (r) {\n      return '<button class=\"hit\" type=\"button\" data-id=\"' + r.id + '\"><b>' + esc(r.name) + '<\/b><span>' + esc(r.type || '') + (r.group ? ' \u00b7 ' + esc(tr('csBy', {g: r.group})) : '') + '<\/span><em>' + esc(r.date || '') + '<\/em><\/button>';\n    }).join('') + '<\/div>';\n  } catch (e) { out.innerHTML = '<p class=\"none\">' + esc(e.message) + '<\/p>'; }\n}\nasync function csRelease(id) {\n  var out = $('rfflCsOut');\n  out.innerHTML = '<p class=\"none\">' + esc(tr('csLoading')) + '<\/p>';\n  try {\n    var r = await (await csFetch({a: 'release', id: id})).json();\n    var h = '<div class=\"rel\">' + (r.screenshot ? '<img decoding=\"async\" src=\"' + esc(r.screenshot) + '\" alt=\"\" loading=\"lazy\" referrerpolicy=\"no-referrer\">' : '<div><\/div>') + '<div><h4>' + esc(r.name) + '<\/h4><div class=\"meta\">' +\n      esc([r.type, r.group ? tr('csBy', {g: r.group}) : '', r.year].filter(Boolean).join(' \u00b7 ')) + ' \u00b7 <a href=\"https:\/\/csdb.dk\/release\/?id=' + (+r.id) + '\" target=\"_blank\" rel=\"noopener\">' + esc(tr('csPage')) + '<\/a><\/div><div class=\"dls\">';\n    if (!r.links || !r.links.length) h += '<p class=\"none\">' + esc(tr('csNoDl')) + '<\/p>';\n    (r.links || []).forEach(function (l) {\n      h += '<div class=\"dl\"><code>' + esc(l.file) + '<\/code><small>' + esc(tr('csDl', {n: l.downloads || 0})) + '<\/small>' +\n        (l.internal ? '<button class=\"btn sm pri\" type=\"button\" data-dl=\"' + (+l.id) + '\" data-fn=\"' + esc(l.file) + '\">' + esc(tr('csOpen')) + '<\/button>' : '<a href=\"' + esc(l.url) + '\" target=\"_blank\" rel=\"noopener nofollow\" title=\"' + esc(tr('csOnly')) + '\">' + esc(tr('csExt')) + '<\/a>') + '<\/div>';\n    });\n    out.innerHTML = h + '<\/div><\/div><\/div>';\n  } catch (e) { out.innerHTML = '<p class=\"none\">' + esc(e.message) + '<\/p>'; }\n}\nasync function csDownload(dlId, fname, btn) {\n  btn.disabled = true; toast(tr('csGetting', {n: fname}));\n  try {\n    var r = await csFetch({a: 'file', id: dlId});\n    var name = r.headers.get('X-Filename') || fname, data = new Uint8Array(await r.arrayBuffer());\n    log('info', 'CSDb: ' + name + ' (' + data.length + ' byte)');\n    await addEntries([{name: name, data: data}], tr('fromCsdb'));\n  } catch (e) { toast(e.message, true); }\n  btn.disabled = false;\n}\n\n\/* ---------- schede ---------- *\/\nfunction showTab(t) {\n  root.querySelectorAll('#rfflPc .tabs button').forEach(function (b) { b.setAttribute('aria-selected', b.dataset.tab === t ? 'true' : 'false'); });\n  $('rfflPFiles').hidden = t !== 'files'; $('rfflPImg').hidden = t !== 'img'; $('rfflPCsdb').hidden = t !== 'csdb'; $('rfflPLog').hidden = t !== 'log';\n  if (t === 'log') logEl.scrollTop = logEl.scrollHeight;\n  if (t === 'img') renderImage();\n}\n\n\/* ---------- trascinamento ---------- *\/\nfunction dropZone(el, onFiles) {\n  var n = 0;\n  el.addEventListener('dragenter', function (e) { if (!e.dataTransfer.types.includes('Files')) return; e.preventDefault(); n++; el.classList.add('over'); });\n  el.addEventListener('dragover', function (e) { if (e.dataTransfer.types.includes('Files')) e.preventDefault(); });\n  el.addEventListener('dragleave', function () { if (--n <= 0) { n = 0; el.classList.remove('over'); } });\n  el.addEventListener('drop', function (e) {\n    e.preventDefault(); n = 0; el.classList.remove('over');\n    if (e.dataTransfer.files.length) onFiles(e.dataTransfer.files);\n  });\n}\n\n\/* ---------- collegamento degli eventi ---------- *\/\nfunction wire() {\n  root.querySelectorAll('.modes button').forEach(function (b) {\n    b.addEventListener('click', async function () {\n      if (st.busy || b.dataset.m === st.mode) return;\n      setMode(b.dataset.m);\n      if (b.dataset.m === 'sim') connectSim();\n      else { await disconnect(); if (st.knownUsb) $('rfflUsbAgain').hidden = false; }\n      renderFlag();\n    });\n  });\n  $('rfflUsbGo').onclick = function () { connectUsb(null); };\n  $('rfflUsbAgain').onclick = function () { connectUsb(st.knownUsb); };\n  $('rfflUsbOff').onclick = function () { disconnect(); $('rfflUsbNote').innerHTML = tr('usbIdle'); };\n  $('rfflSimModel').onchange = connectSim;\n  $('rfflSimDisk').onchange = async function () {\n    var c = st.conn; if (!c || !c.fake || st.busy) return;\n    var d = simDisk(c.fake.model, this.value);\n    if (d) c.fake.insert(d); else c.fake.eject();\n    if (d) await refreshDir(); else { st.dir = null; renderDrive(); showStatus({code: 74, text: '74,DRIVE NOT READY,00,00'}); }\n  };\n  $('rfflSimSpeed').onchange = function () { if (st.conn && st.conn.fake) st.conn.fake.speed = this.value; };\n  $('rfflSimWp').onchange = function () { if (st.conn && st.conn.fake) st.conn.fake.writeProtect = this.checked; };\n  $('rfflSimDl').onclick = function () { var f = st.conn && st.conn.fake; if (f && f.disk) saveBytes('drive-simulato-' + f.model + '.' + f.disk.fmt.id, f.disk.data); };\n  $('rfflDev').onchange = function () {\n    if (!st.conn) return;\n    st.conn.drive.dev = +this.value; st.conn.drive.model = '';\n    if (st.conn.fake) st.conn.fake.dev = +this.value;\n    st.dir = null; renderDrive();\n    op(tr('opSt'), function () { return st.conn.drive.status(); }).then(function (s) { showStatus(s); st.conn.drive.setModel(s); if (!s.none) refreshDir(); else toast(tr('notPresent', {0: st.conn.drive.dev}), true); }, function () {});\n  };\n  $('rfflSt').onclick = function () { op(tr('opSt'), function () { return st.conn.drive.status(); }).then(showStatus, function () {}); };\n  $('rfflId').onclick = function () { op(tr('opId'), function () { return st.conn.drive.identify(); }).then(function (s) { showStatus(s); renderDrive(); }, function () {}); };\n  $('rfflRst').onclick = function () {\n    op(tr('opRst'), async function () { await st.conn.cbm.exclusive(function () { return st.conn.cbm.reset(); }); await RF.util.sleep(st.conn.fake ? 100 : 2000); return st.conn.drive.status(6000); })\n      .then(function (s) { showStatus(s); st.conn.drive.setModel(s); }, function () {});\n  };\n  $('rfflDir').onclick = refreshDir;\n  $('rfflFmt').onclick = async function () {\n    var c = st.conn, cur = st.dir ? S(st.dir.name) : '';\n    var r = await ask({title: tr('fmtT'), body: tr('fmtB'), fields: [{id: 'n', label: tr('fName'), value: cur || 'RETROFIXER', max: 16}, {id: 'i', label: tr('fId'), value: '01', max: 2}, {id: 'q', type: 'check', label: tr('fQuick')}], ok: tr('bFmtGo'), danger: true});\n    if (!r || !r.n.trim()) return;\n    try { var s = await op(tr('opFmt'), function () { return c.drive.format(pet.fromStr(r.n).slice(0, 16), r.q ? null : pet.fromStr(r.i || '01').slice(0, 2)); }); showStatus(s); } catch (e) {}\n    await refreshDir();\n  };\n  $('rfflVal').onclick = async function () {\n    var c = st.conn;\n    if (!(await ask({title: tr('valT'), body: tr('valB'), ok: tr('bValGo')}))) return;\n    try { var s = await op(tr('opVal'), function () { return c.drive.validate(); }); showStatus(s); } catch (e) {}\n    await refreshDir();\n  };\n  $('rfflCmd').onclick = function () {\n    var c = st.conn, changed = false;\n    ask({title: tr('cmdT'), body: tr('cmdB'), fields: [{id: 'c', label: tr('cmdF'), value: 'I0', max: 58}], ok: tr('bSend'), cancel: tr('close'),\n      onSubmit: async function (v, out) {\n        if (!v.c.trim()) return;\n        out.textContent = '\u2026'; out.className = 'out';\n        try {\n          var s = await op(v.c, function () { return c.drive.command(pet.fromStr(v.c), 30000); });\n          out.textContent = s.text; out.className = 'out' + (s.code >= 20 && s.code !== 73 ? ' err' : ''); showStatus(s); changed = true;\n          if (s.code === 73) c.drive.setModel(s);\n        } catch (e) { out.textContent = errMsg(e); out.className = 'out err'; }\n      }}).then(function () { if (changed) refreshDir(); });\n  };\n  $('rfflRdD').onclick = readDiskDlg;\n  $('rfflSep').onclick = driveSeparator;\n  $('rfflFlag').addEventListener('click', function (ev) { if (ev.target.closest('[data-try=sim]')) { setMode('sim'); connectSim(); } });\n  $('rfflWrD').onclick = writeDiskDlg;\n  $('rfflStop').onclick = function () { if (st.conn) st.conn.cbm.abort = true; };\n\n  root.querySelectorAll('#rfflPc .tabs button').forEach(function (b) { b.addEventListener('click', function () { showTab(b.dataset.tab); }); });\n  $('rfflPick').onclick = function () { $('rfflInp').click(); };\n  $('rfflPickDir').onclick = pickFolder;\n  $('rfflInp').onchange = function () { readFiles(this.files, ''); this.value = ''; };\n  $('rfflInpDir').onchange = function () { var fl = Array.prototype.filter.call(this.files, function (f) { return EXT_OK.test(f.name); }).slice(0, 400); readFiles(fl, ''); this.value = ''; };\n  dropZone($('rfflDrop'), function (fl) { readFiles(fl, ''); });\n  dropZone($('rfflDrv'), async function (fl) {\n    var added = await readFiles(fl, '');\n    var toWrite = added.filter(function (f) { return f.kind === 'file'; });\n    if (toWrite.length && st.conn) await writeAll(toWrite);\n  });\n  $('rfflList').addEventListener('click', function (ev) {\n    var b = ev.target.closest('button[data-a]'); if (!b) return;\n    var f = fileById(+b.closest('.fitem').dataset.id); if (!f) return;\n    if (b.dataset.a === 'rm') { st.files.splice(st.files.indexOf(f), 1); renderFiles(); }\n    else if (b.dataset.a === 'open') { openImage(f.disk, f.pc); showTab('img'); }\n    else if (b.dataset.a === 'wr') writeToDrive(f);\n  });\n  $('rfflList').addEventListener('change', function (ev) {\n    var t = ev.target, f = fileById(+t.closest('.fitem').dataset.id); if (!f) return;\n    if (t.dataset.a === 'nm') { var p = pet.fromStr(t.value).slice(0, 16); if (p.length) f.c64 = p; t.value = S(f.c64); }\n    if (t.dataset.a === 'ty') f.type = t.value;\n  });\n  $('rfflWrAll').onclick = function () { writeAll(st.files.filter(function (f) { return f.kind === 'file'; })); };\n  $('rfflClr').onclick = function () { st.files = []; renderFiles(); };\n  $('rfflPImg').addEventListener('click', function (ev) { var b = ev.target.closest('[data-v]'); if (b && !b.disabled) imgTool(b.dataset.v); });\n  $('rfflCsF').onsubmit = function (ev) { ev.preventDefault(); var q = $('rfflCsQ').value.trim(); if (q) csSearch(q); };\n  $('rfflCsOut').addEventListener('click', function (ev) {\n    var h = ev.target.closest('.hit'); if (h) { csRelease(h.dataset.id); return; }\n    var d = ev.target.closest('[data-dl]'); if (d) csDownload(d.dataset.dl, d.dataset.fn, d);\n  });\n  $('rfflLogBus').onchange = function () { logEl.classList.toggle('nobus', !this.checked); };\n  $('rfflLogCp').onclick = function () { navigator.clipboard.writeText(logLines.join('\\n')).then(function () { toast(tr('copied')); }); };\n  $('rfflLogClr').onclick = function () { logLines = []; logEl.innerHTML = ''; };\n}\n\n\/* ---------- avvio ---------- *\/\n$('rfflStats').innerHTML = [RF.Xum1541.supported() ? tr('stUsb') : tr('stUsbNo'), HAS_FS ? tr('stFs') : tr('stFsNo'), tr('stFmt')].join(' \u00b7 ');\nwire();\nusbInit();\nrenderFiles(); renderImage(); renderDrive(); updateButtons();\n\/\/ con WebUSB si parte dal drive reale (scollegato); senza, solo la simulazione ha senso\nvar m0 = RF.Xum1541.supported() ? 'usb' : 'sim';\ntry { m0 = localStorage.getItem('rfflMode') || m0; } catch (e) {}\nif (m0 === 'usb' && RF.Xum1541.supported()) { setMode('usb'); renderDrive(); }\nelse { setMode('sim'); connectSim(); }\n})(window.RFFloppy = window.RFFloppy || {});\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Collega un drive Commodore 1541, 1571 o 1581 al PC con la XUM1541: programma nel browser per copiare dischi in D64 e scrivere floppy, e guida per costruirla con Arduino Pro Micro, firmware e driver.<\/p>","protected":false},"author":1,"featured_media":11578,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-11581","page","type-page","status-publish","has-post-thumbnail","hentry"],"_links":{"self":[{"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/pages\/11581","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/comments?post=11581"}],"version-history":[{"count":13,"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/pages\/11581\/revisions"}],"predecessor-version":[{"id":11598,"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/pages\/11581\/revisions\/11598"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/media\/11578"}],"wp:attachment":[{"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/media?parent=11581"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/categories?post=11581"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/retrofixer.it\/en\/wp-json\/wp\/v2\/tags?post=11581"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}