1// 2// Copyright (c) 2014 Thomas Skibo. 3// All rights reserved. 4// 5// Adapted in 2022 by Freddy Vandriessche. 6// notice: https://raw.githubusercontent.com/RetroAppleJS/RetroAppleJS.github.io/main/LICENSE.md 7// 8// apple2io.js 9 10// oEMU.component.IO is only a dataset referencing to the peripherals, but it should not contain the Apple2IO ojbject, instead hw.io owns the instance of Apple2IO like hw.io.<method> !!! 11if(oEMU===undefined) var oEMU = {"component":{"IO":{"ACTION_MAP":[]}},"system":{"A2P":{"active":true}}} 12 13if(oEMUI===undefined) var oEMUI = {"slotConfig":function(){},"slotsRender":function(){},"deviceBtn":function(){}} // allow tools to include apple2io.js without apple2main.js 14 15 16 17/* 18 * Shared removable-media row used by Disk II drives and SmartPort disk units. 19 * Keep the native <input type="file"> visible: the browser then owns filename 20 * presentation instead of each peripheral inventing a parallel label/state UI. 21 */ 22function EMU_deviceMediaRowHTML(spec) 23{ 24 spec = spec || {}; 25 26 function attr(value) 27 { 28 return String(value==null ? "" : value) 29 .replace(/&/g,"&") 30 .replace(/</g,"<") 31 .replace(/>/g,">") 32 .replace(/\"/g,"""); 33 } 34 35 var label = String(spec.label || "Disk"); 36 var buttonTitle = spec.buttonTitle || (label+": no disk"); 37 var downloadTitle = spec.downloadTitle || "Save disk"; 38 var managedFilename = spec.fileDisplayName!==undefined && spec.fileDisplayName!==null; 39 var fileID = String(spec.fileID || ""); 40 var fileControl = ""; 41 42 if(managedFilename) 43 { 44 fileControl += " <input type=\"file\"" 45 + (spec.fileName ? " name=\""+attr(spec.fileName)+"\"" : "") 46 + (fileID ? " id=\""+attr(fileID)+"\"" : "") 47 + " style=\"display:none\"" 48 + (spec.fileAccept ? " accept=\""+attr(spec.fileAccept)+"\"" : "") 49 + (spec.fileOnChange ? " onchange=\""+spec.fileOnChange+"\"" : "") 50 + ">"; 51 if(fileID) 52 fileControl += " <label class=appbut for=\""+attr(fileID)+"\" style=\"display:inline-block;cursor:pointer\">Choose File</label>"; 53 fileControl += " <span" 54 + (fileID ? " id=\""+attr(fileID+"_name")+"\"" : "") 55 + " style=\"padding-left:6px\">"+attr(spec.fileDisplayName)+"</span>"; 56 } 57 else 58 { 59 fileControl += " <input type=\"file\"" 60 + (spec.fileName ? " name=\""+attr(spec.fileName)+"\"" : "") 61 + (fileID ? " id=\""+attr(fileID)+"\"" : "") 62 + " style=\"display:inline-block\"" 63 + (spec.fileAccept ? " accept=\""+attr(spec.fileAccept)+"\"" : "") 64 + (spec.fileOnChange ? " onchange=\""+spec.fileOnChange+"\"" : "") 65 + ">"; 66 } 67 68 return "" 69 + " <div class=appbut style=\"padding:5px 0px 0px 0px;text-align:left;\">" 70 + " <input type=button method=get class=appbut" 71 + (spec.buttonID ? " id=\""+attr(spec.buttonID)+"\"" : "") 72 + " value=\""+attr(label)+"\"" 73 + " data-empty=\""+attr(spec.buttonDataEmpty || label)+"\" data-loaded=\"\"" 74 + " title=\""+attr(buttonTitle)+"\"" 75 + (spec.buttonOnClick ? " onclick=\""+spec.buttonOnClick+"\"" : "") 76 + (spec.buttonOnMouseOver ? " onmouseover=\""+spec.buttonOnMouseOver+"\"" : "") 77 + (spec.buttonOnMouseOut ? " onmouseout=\""+spec.buttonOnMouseOut+"\"" : "") 78 + ">" 79 + " <form action=\"index.html\"" 80 + (spec.formID ? " id=\""+attr(spec.formID)+"\"" : "") 81 + " style=\"display:inline;\">" 82 + fileControl 83 + " </form>" 84 + " <span style=\"float:right;white-space:nowrap\">" 85 + "<button class=appbut value=\"Download\"" 86 + (spec.downloadID ? " id=\""+attr(spec.downloadID)+"\"" : "") 87 + (spec.downloadDisabled ? " disabled" : (spec.downloadOnClick ? " onclick=\""+spec.downloadOnClick+"\"" : "")) 88 + " title=\""+attr(downloadTitle)+"\">" 89 + "<i class=\"fa fa-cloud-download-alt\"></i></button>" 90 + (function() 91 { 92 var out=""; 93 var actions=Array.isArray(spec.capabilityActions) ? spec.capabilityActions : []; 94 for(var i=0;i<actions.length;i++) 95 { 96 var action=actions[i] || {}; 97 out += "<button class=\"appbut\"" 98 + (action.id ? " id=\""+attr(action.id)+"\"" : "") 99 + (action.disabled ? " disabled" : (action.onClick ? " onclick=\""+action.onClick+"\"" : "")) 100 + " title=\""+attr(action.title || "")+"\">" 101 + "<i class=\""+attr(action.icon || "fa fa-circle")+"\"></i></button>"; 102 } 103 return out; 104 })() 105 + "</span>"
106 + " </div>"; 107} 108 109function Apple2IO(vid,hostHardware) 110{ 111 const bDebug = true; 112 var io = this; 113 hostHardware = hostHardware || null; 114 115 this.slot_ctx = {}; 116 this.slots = []; 117 this.attachments = {}; 118 119 /* 120 * Device hook lists are rebuilt only when attachments or their active 121 * state change. The CPU hot path then performs direct callback calls 122 * without rescanning attachments or testing method types. 123 */ 124 var tickCallbacks = []; 125 var cycleCallbacks = []; 126 127 /* 128 * Monotonic emulated CPU-cycle timebase. This advances only when a 6502 129 * tick actually completes; it is deliberately independent of host RTC. 130 * Slow peripherals can sample it lazily instead of adding another callback 131 * to the one-call-per-CPU-tick hot path. 132 */ 133 var clockTicks = 0; 134 135 function hookActive(device,hook) 136 { 137 var predicate = hook=="tick" ? device.isTickActive : device.isCycleActive; 138 return typeof(predicate)!="function" || predicate.call(device)!==false; 139 } 140 141 function rebuildDeviceHooks() 142 { 143 tickCallbacks.length = 0; 144 cycleCallbacks.length = 0; 145 146 for(var key in io.attachments) 147 { 148 var device = io.attachments[key].device; 149 if(!device) continue; 150 151 if(typeof(device.tick)=="function" && hookActive(device,"tick")) 152 tickCallbacks.push(device.tick.bind(device)); 153 154 if(typeof(device.cycle)=="function" && hookActive(device,"cycle")) 155 cycleCallbacks.push(device.cycle.bind(device)); 156 } 157 } 158 159 this.refreshDeviceHooks = rebuildDeviceHooks; 160 161 // restart() installs the real empty-bus filler; live remounts reuse it. 162 var refillEmptyIOActions = function(){}; 163 164 if(typeof(oEMU.component.IO)!="undefined") 165 { 166 oEMU.component.IO.self = this; 167 } 168 169 170// _____ __ ___ _ _______ _____ 171// |_ _| / /.' `. / \ |_ __ \|_ _| 172// | | / // .-. \ / _ \ | |__) | | | 173// | | / / | | | | / ___ \ | ___/ | | 174// _| |_ / / \ `-' / _/ / \ \_ _| |_ _| |_ 175// |_____|/_/ `.___.' |____| |____||_____| |_____| 176 177 this.reset = function() 178 { 179 // Reset every mounted peripheral by its unique slot ownership. 180 for(var slotN in this.slots) 181 { 182 var peripheral = this.SLOT2obj(slotN); 183 if(peripheral && typeof(peripheral.reset)=="function") 184 peripheral.reset(); 185 } 186 187 for(var deviceKey in this.attachments) 188 { 189 var device = this.attachments[deviceKey].device; 190 if(device && typeof(device.reset)=="function") 191 device.reset(); 192 } 193 194 rebuildDeviceHooks(); 195 } 196 197 this.provisionPeripheral = function(owner,model) 198 { 199 if(!owner) return false; 200 201 if(typeof(owner.IO_map)=="function") 202 mergeActionMap(CIO.ACTION_MAP,owner.IO_map(model)); 203 204 var deviceConfig = Array.isArray(owner.deviceConfig) ? owner.deviceConfig : []; 205 for(var deviceN=0;deviceN<deviceConfig.length;deviceN++) 206 { 207 var deviceInfo=deviceConfig[deviceN]; 208 if(!deviceInfo || deviceInfo.autoAttach===false) continue; 209 210 if(deviceInfo.autoAttach==="if-empty") 211 { 212 var mountedDevices=Array.isArray(owner.devices) ? owner.devices : []; 213 if(mountedDevices.length>0) continue; 214 } 215 216 this.attach(owner,deviceInfo); 217 } 218 219 return true; 220 } 221 222 this.restart = function() 223 { 224 console.assert(Array.isArray(this.slots), "Apple2IO.slots must be initialized"); 225 226 // create empty slot info directly in the Apple2IO-owned configuration 227 if (this.slots.length == 0) 228 { 229 for (var slotN = 0; slotN < slot_count + 1; slotN++) 230 this.slots[slotN] = {"slotTitle": slotN2name(slotN)}; 231 } 232 233 var model = typeof(EMU_system_get)=="function" ? EMU_system_get() : "A2P"; 234 235 update_IORANGES(model); 236 if(!CIO.ACTION_MAP) CIO.ACTION_MAP = emptyActionMap(); 237 238 // _CFG_PSLOT is the whitelist; eligible IO containers are discovered. 239 var pContainers = this.scanPeripheralContainers(); 240 241 // mount peripherals according to default configuration _CFG_PSLOT 242 for(var slotN in slotR.slotMap) 243 { 244 if(isNaN(slotN)) continue; 245 246 var pinfo = pContainers[slotR.slotMap[slotN][0]]; 247 if(pinfo===undefined) continue; 248 249 var cinfo = _CFG_PSLOT[pinfo.PCODE]; 250 var slotObj = this.slots[slotN];
251 if(slotObj===undefined) { continue; } 252 253 if(slotObj.peripheral?.mount?.source == "COM_CONFIG") continue; // avoid re-mounting pre-configured peripherals at the next restart 254 255 var o = this.mount(cinfo,pinfo,slotN,slotR.slotFit[slotN]); 256 if(!o || !o.pObj) continue; 257 258 for(var so in o.sInfo) slotObj[so] = o.sInfo[so]; 259 slotObj.peripheral = o.pObj; 260 261 if(bDebug) console.log("EMU_apple2io.js - mount(<"+pinfo.PCODE+" in "+slotN2name(slotN)+">)" ); 262 } 263 264 // Peripherals may expose host maps and declarative attached devices. 265 // Re-run this on every restart, including when the mounted object is reused. 266 for(var mountedSlotN in this.slots) 267 { 268 var owner = this.slots[mountedSlotN].peripheral; 269 if(!owner) continue; 270 this.provisionPeripheral(owner,model); 271 } 272 273 refillEmptyIOActions = installEmptyIOActions; 274 installEmptyIOActions(); 275 276 function installEmptyIOActions() 277 { 278 function isSafeRead(ctx) { return ctx && ctx.bRO === true; } 279 280 function rd_empty(base,label) 281 { 282 var fn = function(rel_addr,ctx) 283 { 284 if(!isSafeRead(ctx) && bDebug) 285 console.warn( 286 label + ": RD unmapped address at $" + 287 oCOM.getHexWord(0xC000 + base + rel_addr) 288 ); 289 290 return 0x00; 291 }; 292 293 fn._ioEmpty = true; 294 return fn; 295 } 296 297 function wr_empty(base,label) 298 { 299 var fn = function(rel_addr,d8,ctx) 300 { 301 if(!isSafeRead(ctx) && bDebug) 302 console.warn( 303 label + ": WR unmapped address at $" + 304 oCOM.getHexWord(0xC000 + base + rel_addr) 305 ); 306 307 return 0x00; 308 }; 309 310 fn._ioEmpty = true; 311 return fn; 312 } 313 314 /* 315 * $C000-$C07F: Host I/O. 316 * AppleBoard maps the real soft-switches; fill the remaining lines only. 317 */ 318 for(var line = 0x00; line <= 0x70; line += 0x10) 319 { 320 if(!CIO.ACTION_MAP.RD[line]) CIO.ACTION_MAP.RD[line] = rd_empty(line,"HostIO"); 321 if(!CIO.ACTION_MAP.WR[line]) CIO.ACTION_MAP.WR[line] = wr_empty(line,"HostIO"); 322 } 323 324 /* 325 * $C080-$C0FF: Slot I/O. 326 * This fixes the observed $C090 case when slot 1 has no mounted peripheral. 327 */ 328 for(var line = 0x80; line <= 0xF0; line += 0x10) 329 { 330 if(!CIO.ACTION_MAP.RD[line]) CIO.ACTION_MAP.RD[line] = rd_empty(line,"SlotIO"); 331 if(!CIO.ACTION_MAP.WR[line]) CIO.ACTION_MAP.WR[line] = wr_empty(line,"SlotIO"); 332 } 333 334 /* 335 * $C100-$C7FF: Slot ROM. 336 */ 337 for(var line = 0x100; line <= 0x700; line += 0x100) 338 { 339 if(!CIO.ACTION_MAP.RD[line]) CIO.ACTION_MAP.RD[line] = rd_empty(line,"SlotROM"); 340 if(!CIO.ACTION_MAP.WR[line]) CIO.ACTION_MAP.WR[line] = wr_empty(line,"SlotROM"); 341 } 342 343 /* 344 * $C800-$CFFF: Shared expansion ROM / Host ROM area. 345 * $CFFF is the conventional "disable slot C8 ROM" address, but when no 346 * card owns this area, it must still be safely readable. 347 */ 348 for(var line = 0x800; line <= 0xF00; line += 0x100) 349 { 350 if(!CIO.ACTION_MAP.RD[line]) CIO.ACTION_MAP.RD[line] = rd_empty(line,"HostROM"); 351 if(!CIO.ACTION_MAP.WR[line]) CIO.ACTION_MAP.WR[line] = wr_empty(line,"HostROM"); 352 } 353 } 354 355 356/* 357Example output of a mounted peripheral: 358 359this.slots[3] = 360{ 361 "slotTitle":"PR#2", 362 "peripheral": 363 { 364 "id": 365 { 366 "PCODE":"VIDEX", 367 "icon":"fa fa-tv", 368 "coID":"col80card", 369 "description":"Videx Videoterm 80 Column Display" 370 }, 371 "state":{ "active":true }, 372 "action":{ }, 373 "mount": 374 { 375 "slotN":3, 376 "slotFit":["DISKII","VIDEX","MOCK"], 377 "ranges": 378 { 379 "HostROM":{ "from":2048, "to":4095 }, 380 "SlotIO":{ "from":176, "to":191 }, 381 "SlotROM":{ "from":768, "to":1023 } 382 }, 383 "hash":16425 384 } 385 } 386} 387*/ 388 389 //console.log(CIO.ACTION_MAP); 390 //console.group("ACTION_MAP overview"); 391 //console.log("ACTION_MAP size="+oCOM.roughSizeOfObject(CIO.ACTION_MAP)+"bytes"); 392 //console.table(actionMapEntryCount(CIO.ACTION_MAP)); 393 //console.table(actionMapSpanReport(CIO.ACTION_MAP)); 394 //console.groupEnd(); 395 396 console.group("slot configuration overview"); 397 console.table(slotConfigReport(this.slots)); 398 //console.log("this.slots = "+JSON.stringify(this.slots)); 399 console.groupEnd(); 400 401 // restart all plugged-in and active peripherals 402 for(var slotN in this.slots) 403 { 404 if(this.slots[slotN].peripheral===undefined) continue; 405 var peripheral_obj = this.slots[slotN].peripheral; 406 if(peripheral_obj.restart) 407 { 408 peripheral_obj.restart(); 409 if(bDebug) console.log("EMU_apple2io.js - restart(<"+peripheral_obj.id.PCODE+" in "+slotN2name(slotN)+">)" ); 410 } 411 } 412 413 for(var deviceKey in this.attachments) 414 { 415 var device = this.attachments[deviceKey].device; 416 if(device && typeof(device.restart)=="function") 417 { 418 device.restart(); 419 if(bDebug) console.log("EMU_apple2io.js - restart device <"+device.id.DCODE+" on "+device.id.hostPCODE+">"); 420 } 421 } 422 423 rebuildDeviceHooks(); 424 /* 425 * Every mounted peripheral was restarted in the slot loop above. 426 * Do not restart one arbitrarily selected PCODE match a second time. 427 */ 428 429 430 //if(typeof(CIO)=="undefined") var CIO = {ACTION_MAP:{}}; 431 } 432 433function mergeActionMap(dst,src) 434{ 435 if(!dst) dst = emptyActionMap(); 436 if(!src) return dst; 437 438 for(var op in src) 439 { 440 if(!dst[op]) dst[op] = {}; 441 for(var addr in src[op]) 442 dst[op][addr] = src[op][addr]; 443 } 444 445 return dst; 446} 447 448 this.address_encoder = function(rel_addr,bucket,slot) 449 { 450 switch(bucket) 451 { 452 case "HostIO": return rel_addr; 453 case "SlotIO": return rel_addr - 0x80 - (slot<<4); 454 case "SlotROM": return rel_addr - 0x100 - (slot-1<<8); 455 case "HostROM": return rel_addr - 0x800; 456 } 457 } 458 459 /* 460 * Type query: intentionally returns every matching instance. 461 * Never select obj_arr[0] when the caller means a particular slot. 462 */ 463 this.PCODE2obj = function(PCODE) 464 { 465 var obj_arr = []; 466 for(var o in this.slots) 467 { 468 if(this.slots[o].peripheral?.id.PCODE == PCODE) 469 obj_arr.push(this.slots[o].peripheral); 470 } 471 return obj_arr; 472 } 473 474 this.DCODE2obj = function(DCODE,hostPCODE) 475 { 476 var obj_arr = []; 477 478 for(var slotN in this.slots) 479 { 480 var owner = this.slots[slotN].peripheral; 481 if(!owner || (hostPCODE && owner.id?.PCODE != hostPCODE)) continue; 482 483 var devices = Array.isArray(owner.devices) ? owner.devices : []; 484 for(var i=0;i<devices.length;i++) 485 if(devices[i]?.id?.DCODE == DCODE) 486 obj_arr.push(devices[i]); 487 } 488 489 return obj_arr; 490 } 491 492 /* 493 * Pipe addresses identify one named port on one child device: 494 * 495 * <slot>:<DCODE>:<port> 496 * 497 * The first live connection is: 498 * 499 * 0:PASTEBO:text -> 0:A2KBD:text 500 * 501 * Apple2IO only resolves endpoints and validates their contracts. Payload 502 * interpretation belongs to the destination device. 503 */ 504 505 function pipeMimeParse(mime) 506 { 507 var fields = String(mime || "").split(";"); 508 var base = String(fields.shift() || "").trim().toLowerCase(); 509 var params = {}; 510 511 for(var i=0;i<fields.length;i++) 512 { 513 var field = fields[i].trim(); 514 if(!field) continue; 515 516 var eq = field.indexOf("="); 517 if(eq<1) continue; 518 519 var key = field.substring(0,eq).trim().toLowerCase(); 520 var value = field.substring(eq+1).trim(); 521 522 if(value.length>=2 && 523 ((value[0]=='"' && value[value.length-1]=='"') || 524 (value[0]=="'" && value[value.length-1]=="'"))) 525 value = value.substring(1,value.length-1); 526 527 if(key=="charset") value = value.toLowerCase(); 528 if(key) params[key] = value; 529 } 530 531 return {"base":base,"params":params}; 532 } 533 534 function pipeMimeBase(mime) 535 { 536 return pipeMimeParse(mime).base; 537 } 538 539 function pipeMimeNormalize(mime) 540 { 541 var parsed = pipeMimeParse(mime); 542 if(!parsed.base) return ""; 543 544 var keys = Object.keys(parsed.params).sort(); 545 var out = parsed.base; 546 547 for(var i=0;i<keys.length;i++) 548 out += "; "+keys[i]+"="+parsed.params[keys[i]]; 549 550 return out; 551 } 552 553 function pipeMimeList(port) 554 { 555 if(!port || port.mime===undefined || port.mime===null) return []; 556 557 var list = Array.isArray(port.mime) ? port.mime : [port.mime]; 558 var out = []; 559 560 for(var i=0;i<list.length;i++) 561 { 562 var mime = pipeMimeNormalize(list[i]); 563 if(mime) out.push(mime); 564 } 565 566 return out; 567 } 568 569 function pipeMimeSupported(port,mime) 570 { 571 var requested = pipeMimeParse(mime); 572 if(!requested.base) return false;
573 574 var list = pipeMimeList(port); 575 var slash = requested.base.indexOf("/"); 576 var major = slash>=0 577 ? requested.base.substring(0,slash) 578 : requested.base; 579 580 for(var i=0;i<list.length;i++) 581 { 582 var advertised = pipeMimeParse(list[i]); 583 var baseMatch = 584 advertised.base=="*/*" 585 || advertised.base==requested.base 586 || advertised.base==major+"/*"; 587 588 if(!baseMatch) continue; 589 590 /* 591 * Parameters advertised by a port are constraints. Thus 592 * text/plain;charset=us-ascii and text/plain;charset=utf-16le are 593 * distinct contracts, while a generic text/plain port can accept 594 * either representation. 595 */ 596 var keys = Object.keys(advertised.params); 597 var paramsMatch = true; 598 599 for(var k=0;k<keys.length;k++) 600 { 601 var key = keys[k]; 602 if(requested.params[key]!==advertised.params[key]) 603 { 604 paramsMatch = false; 605 break; 606 } 607 } 608 609 if(paramsMatch) return true; 610 } 611 612 return false; 613 } 614 615 function pipeBytePayload(data) 616 { 617 if(data instanceof Uint8Array || data instanceof ArrayBuffer) 618 return true; 619 620 if(typeof(ArrayBuffer)!="undefined" && 621 typeof(ArrayBuffer.isView)=="function" && 622 ArrayBuffer.isView(data)) 623 return true; 624 625 if(Array.isArray(data)) 626 { 627 for(var i=0;i<data.length;i++) 628 if(!Number.isFinite(Number(data[i]))) 629 return false; 630 return true; 631 } 632 633 return false; 634 } 635 636 function pipeMimeCompatible(port,mime,data) 637 { 638 if(pipeMimeSupported(port,mime)) 639 return true; 640 641 /* 642 * application/octet-stream denotes a representation-agnostic byte 643 * carrier. It may transparently carry a more specific semantic MIME 644 * when the actual payload is already bytes; no transcoding occurs. 645 * 646 * Example: 647 * SPGPT text/plain; charset=utf-16le 648 * <-> SPSERIAL application/octet-stream 649 */ 650 return ( 651 pipeBytePayload(data) && 652 pipeMimeSupported(port,"application/octet-stream") 653 ); 654 } 655 656 function pipeDirectionAllows(port,flow) 657 { 658 var direction = String(port && port.direction || "").toLowerCase(); 659 660 if(direction=="duplex") return true; 661 if(flow=="in") return direction=="in" || direction=="input"; 662 if(flow=="out") return direction=="out" || direction=="output"; 663 664 return false; 665 } 666 667 function pipeDevice(owner,DCODE) 668 { 669 var devices = owner && Array.isArray(owner.devices) ? owner.devices : []; 670 671 for(var i=0;i<devices.length;i++) 672 if(String(devices[i]?.id?.DCODE || "").toUpperCase()==DCODE) 673 return devices[i]; 674 675 return null; 676 } 677 678 function pipeDeviceAtSlot(slotAddress,DCODE) 679 { 680 slotAddress = Number(slotAddress); 681 if(!Number.isInteger(slotAddress) || slotAddress<0) return null; 682 683 if(slotAddress==0) 684 { 685 var board = io.SLOT2obj(0); 686 var boardDevice = pipeDevice(board,DCODE); 687 688 if(boardDevice) 689 return { 690 "slotIndex":0 691 ,"owner":board 692 ,"device":boardDevice 693 }; 694 } 695 696 var slotIndex = slotAddress + 1; 697 var owner = io.SLOT2obj(slotIndex); 698 var device = pipeDevice(owner,DCODE); 699 700 return device 701 ? { 702 "slotIndex":slotIndex 703 ,"owner":owner 704 ,"device":device 705 } 706 : null; 707 } 708 709 function pipeEndpointOpen(endpoint) 710 { 711 if(!endpoint || !endpoint.port) return false; 712 713 var open = endpoint.port.open; 714 715 if(open===undefined || open===null) return true; 716 if(typeof(open)=="boolean") return open; 717 718 if(typeof(open)=="string") 719 { 720 var method = endpoint.device && endpoint.device[open]; 721 return typeof(method)=="function" 722 ? method.call(endpoint.device,endpoint)!==false 723 : false;
724 } 725 726 if(typeof(open)=="function") 727 return open.call(endpoint.device,endpoint)!==false; 728 729 return !!open; 730 } 731 732 this.pipeResolve = function(address) 733 { 734 var match = 735 /^(\d+):([A-Za-z0-9_-]+):([A-Za-z0-9_.-]+)$/ 736 .exec(String(address || "").trim()); 737 738 if(!match) return null; 739 740 var slotN = Number(match[1]); 741 var DCODE = match[2].toUpperCase(); 742 var requestedPort = match[3]; 743 var resolved = pipeDeviceAtSlot(slotN,DCODE); 744 if(!resolved) return null; 745 746 var owner = resolved.owner; 747 var device = resolved.device; 748 749 if(!device.ports) return null; 750 751 752 var portName = null; 753 var requestedLower = requestedPort.toLowerCase(); 754 755 for(var name in device.ports) 756 { 757 if(String(name).toLowerCase()==requestedLower) 758 { 759 portName = name; 760 break; 761 } 762 } 763 764 if(portName===null) return null; 765 766 return { 767 "address":slotN+":"+DCODE+":"+portName 768 ,"slotN":slotN 769 ,"slotIndex":resolved.slotIndex 770 ,"owner":owner 771 ,"device":device 772 ,"DCODE":DCODE 773 ,"portName":portName 774 ,"port":device.ports[portName] 775 }; 776 }; 777 778 this.pipeIsOpen = function(address) 779 { 780 return pipeEndpointOpen(this.pipeResolve(address)); 781 } 782 /* 783 * Resolve the semantic text-input endpoint that currently owns user text. 784 * 785 * Motherboard keyboard input is the permanent fallback. An active 786 * VideoTerm takes precedence only while its VIDEXTXT:text port is open, 787 * which is already tied to the VideoTerm video-selection state. 788 * 789 * Both pasteboard text and printable host-keyboard characters use this 790 * selector so they cannot drift into separate character-mapping rules. 791 */ 792 this.getTextInputTargetAddress = function() 793 { 794 const fallback = "0:A2KBD:text"; 795 796 if(!Array.isArray(this.slots) || 797 typeof(this.SLOT2obj)!="function") 798 return fallback; 799 800 for(var slotIndex=1;slotIndex<this.slots.length;slotIndex++) 801 { 802 var owner = this.SLOT2obj(slotIndex); 803 804 if(!owner || 805 String(owner.id?.PCODE || "").toUpperCase()!="VIDEX") 806 continue; 807 808 if(owner.state && owner.state.active===false) 809 continue; 810 811 var slotID = 812 typeof(this.slot2ID)=="function" 813 ? this.slot2ID(slotIndex) 814 : String(slotIndex-1); 815 816 var address = slotID + ":VIDEXTXT:text"; 817 818 if(this.pipeIsOpen(address)) 819 return address; 820 } 821 822 return fallback; 823 }; 824 825 /* 826 * Pipe availability is runtime state, not merely topology. Consumers such 827 * as the pasteboard UI can subscribe once and recompute their preferred 828 * route whenever a device reports that one of its routing conditions 829 * changed. 830 */ 831 var pipeStateListeners = []; 832 833 this.subscribePipeStateChange = function(callback) 834 { 835 if(typeof(callback)!="function") return function(){}; 836 837 pipeStateListeners.push(callback); 838 var subscribed = true; 839 840 return function() 841 { 842 if(!subscribed) return; 843 subscribed = false; 844 845 var index = pipeStateListeners.indexOf(callback); 846 if(index>=0) pipeStateListeners.splice(index,1); 847 }; 848 }; 849 850 this.notifyPipeStateChange = function(change) 851 { 852 var listeners = pipeStateListeners.slice(); 853 854 for(var i=0;i<listeners.length;i++) 855 { 856 try { listeners[i](change || {}); } 857 catch(err) 858 { 859 console.error("Apple2IO pipe-state listener failed",err); 860 } 861 } 862 }; 863 864 this.pipeSend = function(sourceAddress,targetAddress,message) 865 { 866 var source = this.pipeResolve(sourceAddress); 867 var target = this.pipeResolve(targetAddress); 868 869 if(!source || !target) 870 { 871 console.error( 872 "Apple2IO.pipeSend: unresolved pipe endpoint", 873 sourceAddress, 874 targetAddress 875 ); 876 return false;
877 } 878 879 if(!pipeEndpointOpen(source)) 880 { 881 console.error("Apple2IO.pipeSend: source port is closed",source.address); 882 return false; 883 } 884 885 if(!pipeEndpointOpen(target)) 886 { 887 console.error("Apple2IO.pipeSend: target port is closed",target.address); 888 return false; 889 } 890 891 if(!pipeDirectionAllows(source.port,"out")) 892 { 893 console.error("Apple2IO.pipeSend: source port is not an output",source.address); 894 return false; 895 } 896 897 if(!pipeDirectionAllows(target.port,"in")) 898 { 899 console.error("Apple2IO.pipeSend: target port is not an input",target.address); 900 return false; 901 } 902 903 var envelope = Object.assign({},message || {}); 904 var mime = pipeMimeNormalize(envelope.mime); 905 906 if(!mime) 907 { 908 var sourceMimes = pipeMimeList(source.port); 909 mime = sourceMimes.length ? sourceMimes[0] : ""; 910 } 911 912 if(!pipeMimeCompatible(source.port,mime,envelope.data) || 913 !pipeMimeCompatible(target.port,mime,envelope.data)) 914 { 915 console.error( 916 "Apple2IO.pipeSend: incompatible MIME type", 917 mime, 918 source.address, 919 target.address 920 ); 921 return false; 922 } 923 924 envelope.mime = mime; 925 926 var handler = target.port.handler; 927 928 if(typeof(handler)=="string") 929 handler = target.device[handler]; 930 931 if(typeof(handler)!="function" && typeof(target.device.pipeReceive)=="function") 932 handler = target.device.pipeReceive; 933 934 if(typeof(handler)!="function") 935 { 936 console.error( 937 "Apple2IO.pipeSend: target port has no receiver", 938 target.address 939 ); 940 return false; 941 } 942 943 var result = handler.call( 944 target.device, 945 envelope, 946 { 947 "io":this 948 ,"hw":hostHardware 949 ,"source":source 950 ,"target":target 951 } 952 ); 953 954 return result===undefined ? true : result; 955 }; 956 957 /* 958 * Pull one current payload from an output endpoint and route it through the 959 * normal pipeSend() validation/delivery path. 960 * 961 * Pull-capable output ports publish: 962 * 963 * provider: "methodName" 964 * 965 * The provider may return a complete message envelope ({mime,data,...}) or 966 * raw data. Raw data is wrapped using the first MIME advertised by source. 967 */ 968 this.pipeTransfer = function(sourceAddress,targetAddress,request) 969 { 970 var source = this.pipeResolve(sourceAddress); 971 var target = this.pipeResolve(targetAddress); 972 973 if(!source || !target) 974 { 975 console.error( 976 "Apple2IO.pipeTransfer: unresolved pipe endpoint", 977 sourceAddress, 978 targetAddress 979 ); 980 return false; 981 } 982 983 if(!pipeEndpointOpen(source)) 984 { 985 console.error("Apple2IO.pipeTransfer: source port is closed",source.address); 986 return false; 987 } 988 989 if(!pipeEndpointOpen(target)) 990 { 991 console.error("Apple2IO.pipeTransfer: target port is closed",target.address); 992 return false; 993 } 994 995 if(!pipeDirectionAllows(source.port,"out")) 996 { 997 console.error("Apple2IO.pipeTransfer: source port is not an output",source.address); 998 return false; 999 } 1000 1001 if(!pipeDirectionAllows(target.port,"in")) 1002 { 1003 console.error("Apple2IO.pipeTransfer: target port is not an input",target.address); 1004 return false; 1005 } 1006 1007 var provider = source.port.provider; 1008 1009 if(typeof(provider)=="string") 1010 provider = source.device[provider]; 1011 1012 if(typeof(provider)!="function") 1013 { 1014 console.error( 1015 "Apple2IO.pipeTransfer: source port has no provider", 1016 source.address 1017 ); 1018 return false;
1019 } 1020 1021 var produced = provider.call( 1022 source.device, 1023 request || {}, 1024 { 1025 "io":this 1026 ,"hw":hostHardware 1027 ,"source":source 1028 ,"target":target 1029 } 1030 ); 1031 1032 if(produced===undefined || produced===null || produced===false) 1033 return false; 1034 1035 var envelope; 1036 1037 if(produced && 1038 typeof(produced)=="object" && 1039 !Array.isArray(produced) && 1040 produced.mime!==undefined) 1041 { 1042 envelope = Object.assign({},produced); 1043 } 1044 else 1045 { 1046 var sourceMimes = pipeMimeList(source.port); 1047 envelope = { 1048 "mime":sourceMimes.length ? sourceMimes[0] : "" 1049 ,"data":produced 1050 }; 1051 } 1052 1053 return this.pipeSend(sourceAddress,targetAddress,envelope) 1054 ? envelope 1055 : false; 1056 }; 1057 1058 function emptyActionMap() 1059 { 1060 return {"WR":{},"RD":{},"RR":{},"SV":{},"VA":{},"BT":{},"RG":{}}; 1061 } 1062 1063 function unmapPeripheralActions(peripheral_obj) 1064 { 1065 var ranges = peripheral_obj && peripheral_obj.mount ? peripheral_obj.mount.ranges || {} : {}; 1066 var actions = peripheral_obj && peripheral_obj.action ? peripheral_obj.action : {}; 1067 var bindings = [ 1068 ["HostROM","RD"] 1069 ,["HostROM","WR"] 1070 ,["SlotROM","RD"] 1071 ,["SlotROM","WR"] 1072 ,["SlotIO","RD"] 1073 ,["SlotIO","WR"] 1074 ,["HostIO","RD"] 1075 ,["HostIO","WR"] 1076 ]; 1077 1078 for(var b=0;b<bindings.length;b++) 1079 { 1080 var rangeName = bindings[b][0]; 1081 var op = bindings[b][1]; 1082 var range = ranges[rangeName]; 1083 var action = actions[rangeName] && actions[rangeName][op]; 1084 var callback = action && action.callback; 1085 1086 if(!range || typeof(callback)!="function") continue; 1087 1088 for(var addr=range.from;addr<=range.to;addr++) 1089 if(CIO.ACTION_MAP[op][addr] === callback) 1090 delete CIO.ACTION_MAP[op][addr]; 1091 } 1092 } 1093 1094 1095 function unmapAttachedActions(entry) 1096 { 1097 var bindings = entry && Array.isArray(entry.bindings) ? entry.bindings : []; 1098 1099 for(var i=0;i<bindings.length;i++) 1100 { 1101 var binding = bindings[i]; 1102 var map = CIO.ACTION_MAP[binding.op]; 1103 if(!map || map[binding.addr] !== binding.callback) continue; 1104 1105 if(typeof(binding.previous)=="function") map[binding.addr] = binding.previous; 1106 else delete map[binding.addr]; 1107 } 1108 1109 if(entry) entry.bindings = []; 1110 } 1111 1112 function update_IORANGES(sys_model) 1113 { 1114 if(oEMU.system===undefined) return; 1115 if(typeof(_CFG_IORANGES)==="undefined") return; 1116 1117 for(var o in _CFG_IORANGES) 1118 { 1119 if(syscode_has_model(o,sys_model)) 1120 { 1121 oEMU.system["IORANGES"] = _CFG_IORANGES[o]; 1122 1123 oEMU.system["IORANGES-SLOT"] = {}; 1124 for(var r in _CFG_IORANGES[o]) 1125 { 1126 oEMU.system["IORANGES-SLOT"][r] = 1127 oCOM.parseRngExpr(_CFG_IORANGES[o][r],{n:3}); 1128 } 1129 return; 1130 } 1131 } 1132 } 1133 1134 function syscode_has_model(syscodes,sys_model) 1135 { 1136 if(!syscodes || !sys_model) return false; 1137 1138 var needle = String(sys_model).toUpperCase(); 1139 var list = String(syscodes) 1140 .replace(/<br\s*\/?\s*>/gi,"") 1141 .split(","); 1142 1143 for(var i=0;i<list.length;i++) 1144 { 1145 if(oCOM.trim(list[i]).toUpperCase()===needle) return true; 1146 } 1147 return false; 1148 } 1149 1150 var CIO = oEMU.component.IO; 1151 1152 1153 function line_decode(adr) { return adr<256 ? adr & 0xF0 : (adr & 0xFF00); } // line decoder on IO & PROM addressing 1154 1155 this.read = function(rel_addr) 1156 { 1157 var line = line_decode(rel_addr); 1158 1159 const ctx2 = 1160 { 1161 //"keys": keys, 1162 //"snd": snd, 1163 "vid": vid, 1164 "io": this, 1165 "bRO": (typeof(apple2plus) == "object" && apple2plus && apple2plus.hwObj().bRO === true), 1166 "rel_addr": rel_addr, 1167 "line": line, 1168 "abs_addr": 0xC000 + rel_addr 1169 }; 1170 1171 var fn = CIO.ACTION_MAP.RD[line]; 1172 if(!fn) { if(!ctx2.bRO) console.warn("CIO.ACTION_MAP.RD["+oCOM.getHexWord(line)+"] I/O call out of bounds (0x"+oCOM.getHexWord(line+0xC000)+")"); return 0x00; } 1173 1174 /* 1175 * Read-only bus scan policy: 1176 * 1177 * SlotROM / HostROM must still be read normally, because these are real 1178 * CPU-visible ROM bytes, for example Disk II card ROM. 1179 * 1180 * SlotIO reads are different: on many Apple II cards they are soft-switch 1181 * triggers. During bRO scans, do not call those callbacks. 1182 */ 1183 if(ctx2.bRO && fn._ioReport && fn._ioReport.range == "SlotIO") 1184 return 0x00; 1185 1186 return fn(rel_addr-line,ctx2); 1187 } 1188 1189 this.write = function(rel_addr,d8) 1190 { 1191 var line = line_decode(rel_addr); 1192 1193 const ctx2 = 1194 { 1195 //"keys": keys, 1196 //"snd": snd, 1197 "vid": vid, 1198 "io": this, 1199 "bRO": (typeof(apple2plus) == "object" && apple2plus && apple2plus.hwObj().bRO === true), 1200 "rel_addr": rel_addr, 1201 "line": line, 1202 "abs_addr": 0xC000 + rel_addr 1203 }; 1204 1205 var fn = CIO.ACTION_MAP.WR[line]; 1206 1207 /* 1208 * Empty-bus WR fillers must not hide a peripheral's dynamic HostROM 1209 * write handler or an RD-style soft-switch fallback. 1210 */ 1211 if(fn && fn._ioEmpty!==true) 1212 return fn(rel_addr-line,d8,ctx2); 1213 1214 /* 1215 * $C800-$CFFF is a shared expansion window. Its write target must follow 1216 * the same Hslot owner as its read target; permanently installing one 1217 * card's HostROM.WR callback would become stale as soon as another card 1218 * claims C8 space. 1219 * 1220 * Peripheral mount.slotN uses RetroAppleJS' internal +1 convention while 1221 * ACTION_MAP.Hslot stores the real Apple slot number, hence +1 here. 1222 */ 1223 if(line>=0x800 && line<=0xF00 && 1224 CIO.ACTION_MAP.Hslot!==null && 1225 CIO.ACTION_MAP.Hslot!==undefined) 1226 { 1227 var hostOwner = this.SLOT2obj(Number(CIO.ACTION_MAP.Hslot)+1); 1228 var hostWrite = 1229 hostOwner && 1230 hostOwner.action && 1231 hostOwner.action.HostROM && 1232 hostOwner.action.HostROM.WR; 1233 1234 if(hostWrite && typeof(hostWrite.callback)=="function") 1235 return hostWrite.callback(rel_addr-line,d8,ctx2); 1236 } 1237 1238 /* 1239 * Some Apple II soft-switches are triggered by the address access itself. 1240 * The 16K language card / ramcard is the important case here: 1241 * DOS 3.3 may write to $C080-$C08F, while the peripheral historically 1242 * exposed only a read-side SlotIO callback. 1243 * 1244 * Keep this fallback so write accesses can still trigger RD-style 1245 * soft-switch side effects when no explicit WR handler exists. 1246 */ 1247 var rd = CIO.ACTION_MAP.RD[line]; 1248 if(rd && rd._ioEmpty!==true) return rd(rel_addr-line,ctx2); 1249 1250 /* 1251 * Nothing real handled the write. Use the installed empty-WR callback 1252 * now, so diagnostics remain unchanged for genuinely unmapped writes. 1253 */ 1254 if(fn) return fn(rel_addr-line,d8,ctx2); 1255 if(!ctx2.bRO) console.warn("CIO.ACTION_MAP.WR["+oCOM.getHexWord(line)+"] I/O call out of bounds (0x"+oCOM.getHexWord(line+0xC000)+")"); 1256 1257 return 0x00; 1258 } 1259 1260 // CPU-tick hook for devices that need sub-cycle sampling, such as the speaker. 1261 this.tick = function(n) 1262 { 1263 clockTicks++; 1264 for(var i=0;i<tickCallbacks.length;i++) 1265 tickCallbacks[i](n); 1266 } 1267 1268 this.getClockTicks = function() 1269 { 1270 return clockTicks; 1271 } 1272 1273 // Processing-cycle hook, called once after the configured CPU-tick group. 1274 this.cycle = function() 1275 { 1276 for(var i=0;i<cycleCallbacks.length;i++) 1277 cycleCallbacks[i](); 1278 1279 1280 1281 /* 1282 * Mounted peripherals with slow independent timing domains (UARTs, 1283 * timers, etc.) can synchronize once per processing slice as well as 1284 * lazily on their own register accesses. Passing the absolute counter 1285 * avoids loss of time when processing slices are shortened by turbo 1286 * host-capacity limits. 1287 */ 1288 for(var slotN in this.slots) 1289 { 1290 var peripheral = this.slots[slotN] && this.slots[slotN].peripheral; 1291 // Mounted cards share the same processing-frame cadence as devices. 1292 // In particular, camera sampling follows the SYSTEM FPS scheduler. 1293 if(peripheral && typeof(peripheral.cycle)=="function" && hookActive(peripheral,"cycle")) 1294 peripheral.cycle(); 1295 if(peripheral && typeof(peripheral.syncClock)=="function") 1296 peripheral.syncClock(clockTicks); 1297 } 1298 } 1299 1300 this.attach = function(owner,device_info,options) 1301 { 1302 if(!owner || !owner.id?.PCODE || !device_info || !device_info.coID) return null; 1303 1304 options=options || {}; 1305 var newInstance=options.newInstance===true; 1306 var hostPCODE=owner.id.PCODE; 1307 var dcode=device_info.DCODE || device_info.coID; 1308 if(device_info.hostPCODE && device_info.hostPCODE != hostPCODE) return null; 1309 1310 var ownerHash=owner.mount && owner.mount.hash!==undefined 1311 ? owner.mount.hash 1312 : hostPCODE; 1313 var entry=null; 1314 var key=""; 1315 var device=null; 1316 var createdInstance=false; 1317 1318 /* 1319 * Normal provisioning is idempotent: it reuses the one declarative 1320 * instance for this owner/device type. An explicit UI attach requests
1321 * a new mounted instance instead, even when the DCODE is identical. 1322 */ 1323 if(!newInstance) 1324 { 1325 for(var existingKey in this.attachments) 1326 { 1327 var existing=this.attachments[existingKey]; 1328 if(existing && existing.owner===owner && 1329 existing.device?.id?.DCODE===dcode && 1330 existing.explicitInstance!==true) 1331 { 1332 entry=existing; 1333 key=existingKey; 1334 device=existing.device; 1335 break; 1336 } 1337 } 1338 } 1339 1340 if(entry) unmapAttachedActions(entry); 1341 1342 if(!device) 1343 { 1344 var Device=globalThis[device_info.coID]; 1345 if(typeof(Device)!="function") return null; 1346 1347 device=new Device(device_info); 1348 createdInstance=true; 1349 if(!device.id) device.id={}; 1350 if(device.id.DCODE && device.id.DCODE != dcode) return null; 1351 if(device.id.hostPCODE && device.id.hostPCODE != hostPCODE) return null; 1352 1353 /* 1354 * Device instance identity mirrors peripheral mount identity: a 1355 * stable 16-bit hash belongs to this mounted object for its lifetime. 1356 * The registry key includes it, so equal DCODEs do not collide. 1357 */ 1358 var instanceHash; 1359 var attempts=0; 1360 do 1361 { 1362 instanceHash=oCOM.crc16(new TextEncoder("utf-8").encode( 1363 String(ownerHash)+":"+dcode+":"+Math.random()+":"+attempts 1364 )); 1365 key=String(ownerHash)+":"+dcode+":"+instanceHash; 1366 attempts++; 1367 } 1368 while(this.attachments[key] && attempts<65536); 1369 1370 if(this.attachments[key]) return null; 1371 entry={ 1372 "owner":owner 1373 ,"device":device 1374 ,"info":device_info 1375 ,"bindings":[] 1376 ,"explicitInstance":newInstance 1377 ,"hash":instanceHash 1378 }; 1379 this.attachments[key]=entry; 1380 } 1381 1382 device.id.DCODE=dcode; 1383 device.id.hostPCODE=hostPCODE; 1384 device.id.coID=device_info.coID; 1385 if(device_info.icon!==undefined) device.id.icon=device_info.icon; 1386 if(device_info.description!==undefined) device.id.description=device_info.description; 1387 if(device_info.deviceN!==undefined && device.id.deviceN===undefined) 1388 device.id.deviceN=Number(device_info.deviceN); 1389 1390 if(!Array.isArray(owner.devices)) 1391 Object.defineProperty(owner,"devices",{ 1392 "value":[] 1393 ,"writable":true 1394 ,"configurable":true 1395 ,"enumerable":false 1396 }); 1397 if(owner.devices.indexOf(device)<0) owner.devices.push(device); 1398 1399 entry.owner=owner; 1400 entry.info=device_info; 1401 entry.bindings=[]; 1402 1403 var attachHash=entry.hash!==undefined 1404 ? entry.hash 1405 : device.attach && device.attach.hash!==undefined 1406 ? device.attach.hash 1407 : oCOM.crc16(new TextEncoder("utf-8").encode(key)); 1408 entry.hash=attachHash; 1409 device.attach={ 1410 "hostPCODE":hostPCODE 1411 ,"ownerHash":ownerHash 1412 ,"range":device_info.range || "HostIO" 1413 ,"hash":attachHash 1414 ,"actions":[] 1415 }; 1416 1417 if(typeof(device.bindHost)=="function") 1418 { 1419 var rollbackHostBind=function() 1420 { 1421 if(!createdInstance) return; 1422 delete io.attachments[key]; 1423 var failedIdx=owner.devices.indexOf(device); 1424 if(failedIdx>=0) owner.devices.splice(failedIdx,1); 1425 }; 1426 var hostResult; 1427 try 1428 { 1429 hostResult=device.bindHost(owner); 1430 } 1431 catch(e) 1432 { 1433 rollbackHostBind(); 1434 throw e; 1435 } 1436 if(hostResult===false) 1437 { 1438 rollbackHostBind(); 1439 return null; 1440 } 1441 } 1442 1443 if(typeof(device.bindIO)=="function") 1444 device.bindIO(io); 1445 1446 var actionMap=device_info.action || {}; 1447 for(var op in actionMap) 1448 { 1449 op=String(op).toUpperCase(); 1450 if(!CIO.ACTION_MAP[op]) continue; 1451 1452 for(var address in actionMap[op]) 1453 { 1454 var addr=Number(address); 1455 var handler=actionMap[op][address]; 1456 var method=typeof(handler)=="string" 1457 ? device[handler] 1458 : (handler && typeof(handler.handler)=="string" 1459 ? device[handler.handler] 1460 : (handler && handler.callback ? handler.callback : handler)); 1461 var allowReadOnly=!!( 1462 handler && typeof(handler)=="object" && handler.readOnly===true 1463 ); 1464 1465 if(!Number.isInteger(addr) || typeof(method)!="function") continue; 1466 1467 var callback=function(target,fn,readOnly,writeAction) 1468 { 1469 if(writeAction) 1470 { 1471 return function(rel_addr,d8,ctx) 1472 { 1473 if(ctx && ctx.bRO===true && !readOnly) return 0x00; 1474 var result=fn.call(target,rel_addr,d8,ctx); 1475 return result===undefined ? 0x00 : result; 1476 }; 1477 } 1478 1479 return function(rel_addr,ctx) 1480 { 1481 if(ctx && ctx.bRO===true && !readOnly) return 0x00; 1482 var result=fn.call(target,rel_addr,ctx); 1483 return result===undefined ? 0x00 : result; 1484 }; 1485 }(device,method,allowReadOnly,op=="WR"); 1486 1487 callback._ioReport={ 1488 "DCODE":dcode 1489 ,"hostPCODE":hostPCODE 1490 ,"slotTitle":owner.mount ? slotN2name(owner.mount.slotN) : "attached" 1491 ,"range":device.attach.range 1492 ,"op":op 1493 ,"hash":attachHash 1494 }; 1495 1496 var previous=CIO.ACTION_MAP[op][addr]; 1497 CIO.ACTION_MAP[op][addr]=callback; 1498 entry.bindings.push({"op":op,"addr":addr,"callback":callback,"previous":previous}); 1499 device.attach.actions.push({"op":op,"addr":addr,"handler":typeof(handler)=="string" ? handler : (handler.handler || method.name || "callback")}); 1500 } 1501 } 1502 1503 if(device_info.alias && oEMU.component && oEMU.component.IO) 1504 Object.defineProperty(oEMU.component.IO,device_info.alias,{ 1505 "value":device 1506 ,"writable":true 1507 ,"configurable":true 1508 ,"enumerable":false 1509 }); 1510 1511 Object.defineProperty(device,"_ioRefreshHooks",{ 1512 "value":rebuildDeviceHooks 1513 ,"writable":true 1514 ,"configurable":true 1515 ,"enumerable":false 1516 }); 1517 1518 Object.defineProperty(device,"_ioPipeStateChanged",{
1519 "value":function(change) 1520 { 1521 io.notifyPipeStateChange(Object.assign({ 1522 "type":"device-state" 1523 ,"DCODE":device.id?.DCODE || "" 1524 ,"instanceID":device.attach?.hash 1525 },change || {})); 1526 } 1527 ,"writable":true 1528 ,"configurable":true 1529 ,"enumerable":false 1530 }); 1531 1532 rebuildDeviceHooks(); 1533 1534 this.notifyPipeStateChange({ 1535 "type":"attach" 1536 ,"DCODE":dcode 1537 ,"hostPCODE":hostPCODE 1538 ,"instanceID":attachHash 1539 }); 1540 1541 if(newInstance && typeof(owner.onDeviceTopologyChanged)==="function") 1542 { 1543 try 1544 { 1545 owner.onDeviceTopologyChanged({ 1546 "type":"attach" 1547 ,"DCODE":dcode 1548 ,"device":device 1549 ,"instanceID":attachHash 1550 }); 1551 } 1552 catch(e) { console.error("Device topology attach notification failed",e); } 1553 } 1554 1555 if(bDebug) 1556 console.log("EMU_apple2io.js - attach(<"+dcode+" #"+oCOM.getHexWord(attachHash)+" to "+hostPCODE+">)"); 1557 1558 return device; 1559 } 1560 1561 this.detach = function(owner,DCODE,instanceHash) 1562 { 1563 if(!owner) return false; 1564 var removed = false; 1565 1566 for(var key in this.attachments) 1567 { 1568 var entry = this.attachments[key]; 1569 var device = entry.device; 1570 if(entry.owner !== owner || (DCODE && device?.id?.DCODE != DCODE)) continue; 1571 if(instanceHash!==undefined && instanceHash!==null && 1572 Number(device?.attach?.hash)!==Number(instanceHash)) continue; 1573 1574 /* 1575 * Let a child release any host-owned state before the generic 1576 * attachment record disappears. Liron/SmartPort needs this so an 1577 * ejected UniDisk does not remain resident on the SmartPort bus. 1578 */ 1579 if(device && typeof(device.unbindHost)=="function") 1580 { 1581 try 1582 { 1583 if(device.unbindHost(owner)===false) continue; 1584 } 1585 catch(e) 1586 { 1587 console.error("Device host detach failed",e); 1588 continue; 1589 } 1590 } 1591 1592 unmapAttachedActions(entry); 1593 if(entry.info.alias && oEMU.component.IO[entry.info.alias] === device) 1594 delete oEMU.component.IO[entry.info.alias]; 1595 1596 if(Array.isArray(owner.devices)) 1597 { 1598 var idx = owner.devices.indexOf(device); 1599 if(idx>=0) owner.devices.splice(idx,1); 1600 } 1601 1602 delete this.attachments[key]; 1603 removed = true; 1604 1605 if(typeof(owner.onDeviceTopologyChanged)==="function") 1606 { 1607 try 1608 { 1609 owner.onDeviceTopologyChanged({ 1610 "type":"detach" 1611 ,"DCODE":device?.id?.DCODE || DCODE || "" 1612 ,"device":device 1613 ,"instanceID":Number(device?.attach?.hash) 1614 }); 1615 } 1616 catch(e) { console.error("Device topology detach notification failed",e); } 1617 } 1618 } 1619 1620 if(removed) 1621 { 1622 rebuildDeviceHooks(); 1623 this.notifyPipeStateChange({ 1624 "type":"detach" 1625 ,"DCODE":DCODE || "" 1626 ,"instanceID":instanceHash===undefined ? null : Number(instanceHash) 1627 }); 1628 } 1629 return removed; 1630 } 1631 1632 this.detachInstance = function(owner,instanceHash) 1633 { 1634 instanceHash=Number(instanceHash); 1635 if(!owner || !Number.isInteger(instanceHash)) return false; 1636 return this.detach(owner,null,instanceHash); 1637 }; 1638 1639 this.unmount = function(slotN) 1640 { 1641 slotN = Number(slotN); 1642 var slot = this.slots[slotN]; 1643 1644 if(!Number.isInteger(slotN) || !slot || slot.lock || !slot.peripheral) 1645 return false;
1646 1647 var peripheral_obj = slot.peripheral; 1648 this.detach(peripheral_obj); 1649 1650 unmapPeripheralActions(peripheral_obj); 1651 1652 if(this.MEMORY_MAP && typeof(this.MEMORY_MAP.unmount)=="function") 1653 this.MEMORY_MAP.unmount(peripheral_obj); 1654 1655 if(typeof(peripheral_obj.onUnmount)=="function") 1656 peripheral_obj.onUnmount(); 1657 1658 delete slot.peripheral; 1659 refillEmptyIOActions(); 1660 1661 if(bDebug) 1662 console.log("EMU_apple2io.js - unmount(<"+peripheralPCODE(peripheral_obj)+" from "+slotN2name(slotN)+">)"); 1663 1664 return true; 1665 } 1666 1667 this.mount = function(cinfo,peripheral_info,slotIdx,slotFit,peripheral_obj) 1668 { 1669 var remounting = peripheral_obj != null; 1670 var slot_info = {}; 1671 1672 if(oEMU.system===undefined) return {"sInfo":slot_info,"pObj":null}; 1673 if(!cinfo || !peripheral_info) return {"sInfo":slot_info,"pObj":null}; 1674 if(!oEMU.system["IORANGES"]) return {"sInfo":slot_info,"pObj":null}; 1675 if(!remounting && typeof(peripheral_info.ctor)!="function") 1676 return {"sInfo":slot_info,"pObj":null}; 1677 1678 slotIdx = Number(slotIdx); 1679 if(!Number.isInteger(slotIdx)) 1680 return {"sInfo":slot_info,"pObj":null}; 1681 1682 if(Array.isArray(slotFit) && slotFit.length>0 && 1683 slotFit.indexOf(peripheral_info.PCODE)<0) 1684 return {"sInfo":slot_info,"pObj":null}; 1685 1686 if(oEMU.system["IORANGES"] && peripheral_info) 1687 { 1688 const bHostROM = typeof(oEMU.system.IORANGES.HostROM)!="undefined" && cinfo.HostROM == "X"; 1689 const bSlotIO = typeof(oEMU.system.IORANGES.SlotIO) !="undefined" && cinfo.SlotIO == "X"; 1690 const bSlotROM = typeof(oEMU.system.IORANGES.SlotROM)!="undefined" && cinfo.SlotROM == "X"; 1691 const bHostIO = typeof(oEMU.system.IORANGES.HostIO) !="undefined" && cinfo.HostIO == "X"; 1692 1693 var ranges = {}; 1694 var ioBase = oCOM.parseRngExpr(oEMU.system.IORANGES.HostIO).from; // usually $C000 = base reference for the I/O address space 1695 1696 if(bHostROM) ranges.HostROM = oCOM.parseRngExpr(oEMU.system.IORANGES.HostROM,{n:slotIdx,base:ioBase}); 1697 if(bSlotIO) ranges.SlotIO = oCOM.parseRngExpr(oEMU.system.IORANGES.SlotIO, {n:slotIdx-1,base:ioBase}); 1698 if(bSlotROM) ranges.SlotROM = oCOM.parseRngExpr(oEMU.system.IORANGES.SlotROM,{n:slotIdx-1,base:ioBase}); 1699 if(bHostIO) ranges.HostIO = oCOM.parseRngExpr(oEMU.system.IORANGES.HostIO, {n:slotIdx,base:ioBase}); 1700 1701 // Reuse a live object when moving it; constructing a new one would lose device state. 1702 if(remounting) unmapPeripheralActions(peripheral_obj); 1703 else peripheral_obj = new peripheral_info.ctor(); 1704 //if(cinfo!=null) peripheral_obj.bFirstConfig = true; 1705 1706 if(peripheral_obj) 1707 { 1708 var CIO = oEMU.component.IO; 1709 1710 // Keep static identity on the peripheral object itself. 1711 if(!peripheral_obj.id) peripheral_obj.id = {}; 1712 if(peripheral_obj.id.PCODE===undefined) peripheral_obj.id.PCODE = peripheral_info.PCODE; 1713 if(peripheral_obj.id.icon===undefined) peripheral_obj.id.icon = peripheral_info.icon || "fa fa-cube"; 1714 peripheral_obj.id.coID = peripheral_info.coID || peripheral_obj.constructor.name; 1715 peripheral_obj.id.description = cinfo.NAME; 1716 1717 // Keep stable ownership metadata and dynamic mappings during a live move. 1718 var oldMount = peripheral_obj.mount || {}; 1719 var mountHash = oldMount.hash; 1720 if(mountHash===undefined) 1721 mountHash = oCOM.crc16(new TextEncoder("utf-8").encode(Math.random())); 1722 1723 peripheral_obj.mount = Object.assign({},oldMount,{ 1724 "slotN":slotIdx 1725 ,"slotFit":slotFit || [] 1726 ,"ranges":ranges 1727 ,"hash":mountHash 1728 ,"source":oldMount.source || (cinfo!=null?"COM_CONFIG":"USER") 1729 }); 1730 1731 //if(slotIdx == 7) 1732 // alert("mounted DISKII "+peripheral_obj.mount.hash) 1733 1734 /* 1735 if (peripheral_obj.id && peripheral_obj.id.PCODE == "MS16K") // temporary patch!!! making sure oEMU.component.IO.RamCard (old) remains in sync with peripheral_obj, so that mem monitoring uses the same object between apple ram updates in EMU_apple2hw.js and ramcard ram updates in EMU_CARD_ramcard.js 1736 { 1737 this.ramcard = peripheral_obj; 1738 oEMU.component.IO.RamCard = peripheral_obj; 1739 } 1740 */ 1741 1742 if(peripheral_info.slotLock==true) { slot_info.lock = true; } // inform the slot to never move this peripheral 1743 1744 if(peripheral_obj.action) 1745 { 1746 const _act = peripheral_obj.action; // ACTION_MAP 1747 1748 const _bHostROM = !(_act.HostROM === undefined);
1749 const _bSlotIO = !(_act.SlotIO === undefined); 1750 const _bSlotROM = !(_act.SlotROM === undefined); 1751 const _bHostIO = !(_act.HostIO === undefined); 1752 1753 // PROVIDE THE BASE ADDRESS FOR EACH MEMORY SPACE 1754 if(_bHostROM && ranges.HostROM) _act.HostROM.base = ranges.HostROM.from; 1755 if(_bSlotROM && ranges.SlotROM) _act.SlotROM.base = ranges.SlotROM.from; 1756 if(_bSlotIO && ranges.SlotIO) _act.SlotIO.base = ranges.SlotIO.from; 1757 if(_bHostIO && ranges.HostIO) _act.HostIO.base = ranges.HostIO.from; 1758 1759 // Add non-functional metadata to callbacks so diagnostics can show 1760 // the actual mounted owner behind each ACTION_MAP span. 1761 tagActionCallback(_act.HostROM && _act.HostROM.RD, "HostROM", "RD"); 1762 tagActionCallback(_act.HostROM && _act.HostROM.WR, "HostROM", "WR"); 1763 tagActionCallback(_act.SlotROM && _act.SlotROM.RD, "SlotROM", "RD"); 1764 tagActionCallback(_act.SlotROM && _act.SlotROM.WR, "SlotROM", "WR"); 1765 tagActionCallback(_act.SlotIO && _act.SlotIO.RD, "SlotIO", "RD"); 1766 tagActionCallback(_act.SlotIO && _act.SlotIO.WR, "SlotIO", "WR"); 1767 tagActionCallback(_act.HostIO && _act.HostIO.RD, "HostIO", "RD"); 1768 tagActionCallback(_act.HostIO && _act.HostIO.WR, "HostIO", "WR"); 1769 /* 1770 * HostROM.WR is deliberately not installed here: HostROM is a 1771 * shared C8 window and write dispatch follows ACTION_MAP.Hslot 1772 * dynamically in Apple2IO.write(). 1773 */ 1774 if(_bHostROM && _act.HostROM.RD && ranges.HostROM) { for(var i=ranges.HostROM.from;i<=ranges.HostROM.to;i++) CIO.ACTION_MAP.RD[i] = _act.HostROM.RD.callback; } 1775 if(_bSlotROM && _act.SlotROM.RD && ranges.SlotROM) { for(var i=ranges.SlotROM.from;i<=ranges.SlotROM.to;i++) CIO.ACTION_MAP.RD[i] = _act.SlotROM.RD.callback; } 1776 if(_bSlotROM && _act.SlotROM.WR && ranges.SlotROM) { for(var i=ranges.SlotROM.from;i<=ranges.SlotROM.to;i++) CIO.ACTION_MAP.WR[i] = _act.SlotROM.WR.callback; } 1777 if(_bSlotIO && _act.SlotIO.RD && ranges.SlotIO) { for(var i=ranges.SlotIO.from; i<=ranges.SlotIO.to;i++) CIO.ACTION_MAP.RD[i] = _act.SlotIO.RD.callback; } 1778 if(_bSlotIO && _act.SlotIO.WR && ranges.SlotIO) { for(var i=ranges.SlotIO.from; i<=ranges.SlotIO.to;i++) CIO.ACTION_MAP.WR[i] = _act.SlotIO.WR.callback; } 1779 if(_bHostIO && _act.HostIO.RD && ranges.HostIO) { for(var i=ranges.HostIO.from;i<=ranges.HostIO.to;i++) CIO.ACTION_MAP.RD[i] = _act.HostIO.RD.callback; } 1780 if(_bHostIO && _act.HostIO.WR && ranges.HostIO) { for(var i=ranges.HostIO.from;i<=ranges.HostIO.to;i++) CIO.ACTION_MAP.WR[i] = _act.HostIO.WR.callback; } 1781 } 1782 1783 function tagActionCallback(action,rangeName,op) 1784 { 1785 if(!action || typeof action.callback !== "function") return; 1786 1787 action.callback._ioReport = 1788 { 1789 "PCODE": peripheralPCODE(peripheral_obj) 1790 ,"slotTitle": slotN2name(slotIdx) 1791 ,"range": rangeName 1792 ,"op": op 1793 ,"hash": peripheral_obj.mount.hash 1794 }; 1795 } 1796 if(remounting) refillEmptyIOActions(); 1797 } 1798 } 1799 1800 return {"sInfo":slot_info,"pObj":peripheral_obj}; 1801 } 1802 1803 1804 /* 1805 * Dynamic CPU memory mappings owned by peripherals. 1806 * 1807 * A peripheral registers a rule function. The rule receives a 1808 * JSON-compatible state object and returns the complete mapping 1809 * selected by that state. 1810 * 1811 * handler: 1812 * "@default" -> restore the Apple II default callback 1813 * "methodName" -> use owner[methodName] 1814 */ 1815 this.MEMORY_MAP = 1816 { 1817 rules: {}, 1818 evidence: {}, 1819 1820 addRule: function(id,ruleFunction) 1821 { 1822 if(!id || typeof ruleFunction != "function") 1823 return false;
1824 1825 this.rules[id] = ruleFunction; 1826 return true; 1827 }, 1828 1829 runRule: function(id,state) 1830 { 1831 var ruleFunction = this.rules[id]; 1832 1833 if(typeof ruleFunction != "function") 1834 return false; 1835 1836 return this.mount(ruleFunction(state || {})); 1837 }, 1838 1839 mount: function(rule) 1840 { 1841 var hw = apple2plus.hwObj(); 1842 1843 if(!rule || !rule.owner || !Array.isArray(rule.mappings)) 1844 return false; 1845 1846 if(!hw || !hw.RD || !hw.WR || !hw.default_map) 1847 return false; 1848 1849 var owner = rule.owner; 1850 var hash = owner.mount && owner.mount.hash; 1851 1852 if(hash === undefined) 1853 return false; 1854 1855 var state = cloneMappingState(rule.state); 1856 var evidence = []; 1857 1858 for(var i=0;i<rule.mappings.length;i++) 1859 { 1860 var mapping = rule.mappings[i] || {}; 1861 var op = String(mapping.op || "").toUpperCase(); 1862 var range = parseCPUMappingRange(mapping.range); 1863 1864 if(op != "RD" && op != "WR") 1865 continue; 1866 1867 if(!range) 1868 continue; 1869 1870 /* 1871 * Apple2Hw currently dispatches CPU memory through one 1872 * callback per 4K line. 1873 */ 1874 if( 1875 (range.from & 0x0FFF) != 0 || 1876 (range.to & 0x0FFF) != 0x0FFF 1877 ) 1878 { 1879 console.warn( 1880 "MEMORY_MAP: range must be aligned to 4K lines: " 1881 + mapping.range 1882 ); 1883 1884 continue; 1885 } 1886 1887 var callback = null; 1888 1889 if(mapping.handler != "@default") 1890 { 1891 if(typeof mapping.callback == "function") 1892 { 1893 callback = mapping.callback; 1894 } 1895 else if( 1896 mapping.handler && 1897 typeof owner[mapping.handler] == "function" 1898 ) 1899 { 1900 callback = owner[mapping.handler].bind(owner); 1901 } 1902 1903 if(typeof callback != "function") 1904 { 1905 console.warn( 1906 "MEMORY_MAP: callback not found: " 1907 + mapping.handler 1908 ); 1909 1910 continue; 1911 } 1912 } 1913 1914 for( 1915 var address=range.from; 1916 address<=range.to; 1917 address+=0x1000 1918 ) 1919 { 1920 var line = hw.lineDecode(address); 1921 1922 hw[op][line] = 1923 mapping.handler == "@default" 1924 ? hw.default_map[op][line] 1925 : callback; 1926 } 1927 1928 /* 1929 * Store only serialisable evidence. Function references 1930 * remain in the actual hardware callback tables. 1931 */ 1932 evidence.push({ 1933 "id": mapping.id || op+"_"+range.from 1934 ,"space": mapping.space || "MEMORY MAPPING" 1935 ,"op": op 1936 ,"from": range.from 1937 ,"to": range.to 1938 ,"handler": mapping.handler || "" 1939 ,"target": mapping.target || "" 1940 ,"enabled": mapping.enabled === undefined 1941 ? true 1942 : !!mapping.enabled 1943 ,"condition": mapping.condition || "" 1944 ,"source": rule.source || "" 1945 ,"state": state 1946 }); 1947 } 1948 1949 this.evidence[hash] = evidence; 1950 1951 /* 1952 * Also place the evidence directly in slotConfig[n], beneath 1953 * peripheral.mount, so the slot popup has one source. 1954 */ 1955 if(!owner.mount) 1956 owner.mount = {}; 1957 1958 owner.mount.mappings = evidence; 1959 1960 return evidence.length > 0; 1961 }, 1962 1963 getEvidence: function(owner) 1964 { 1965 var hash = owner && owner.mount && owner.mount.hash; 1966 1967 return hash === undefined 1968 ? [] 1969 : this.evidence[hash] || []; 1970 }, 1971 1972 unmount: function(owner) 1973 {
1974 var hw = typeof(apple2plus)=="object" && apple2plus 1975 ? apple2plus.hwObj() 1976 : null; 1977 1978 if(!owner || !owner.mount || !hw || !hw.RD || !hw.WR || !hw.default_map) 1979 return false; 1980 1981 var hash = owner.mount.hash; 1982 var mappings = Array.isArray(owner.mount.mappings) 1983 ? owner.mount.mappings 1984 : (this.evidence[hash] || []); 1985 1986 for(var i=0;i<mappings.length;i++) 1987 { 1988 var mapping = mappings[i] || {}; 1989 var op = String(mapping.op || "").toUpperCase(); 1990 var from = Number(mapping.from); 1991 var to = Number(mapping.to); 1992 1993 if((op!="RD" && op!="WR") || !Number.isFinite(from) || !Number.isFinite(to)) 1994 continue; 1995 1996 for(var address=from;address<=to;address+=0x1000) 1997 { 1998 var line = hw.lineDecode(address); 1999 hw[op][line] = hw.default_map[op][line]; 2000 } 2001 } 2002 2003 delete this.evidence[hash]; 2004 delete this.rules[peripheralPCODE(owner)+":"+hash]; 2005 owner.mount.mappings = []; 2006 return true; 2007 } 2008 }; 2009 2010 function parseCPUMappingRange(spec) 2011 { 2012 var range; 2013 2014 if(typeof spec == "string") 2015 { 2016 range = oCOM.parseRngExpr(spec); 2017 } 2018 else if(spec && typeof spec == "object") 2019 { 2020 range = { 2021 "from": Number(spec.from) 2022 ,"to": spec.to === undefined 2023 ? Number(spec.from) 2024 : Number(spec.to) 2025 }; 2026 } 2027 2028 if( 2029 !range || 2030 !Number.isFinite(range.from) || 2031 !Number.isFinite(range.to) 2032 ) 2033 return null; 2034 2035 return range; 2036 } 2037 2038 function cloneMappingState(state) 2039 { 2040 try 2041 { 2042 return JSON.parse(JSON.stringify(state || {})); 2043 } 2044 catch(e) 2045 { 2046 return {}; 2047 } 2048 } 2049 2050 // SLOT MAPPING 2051 /* 2052 * Discover peripheral types from the existing IO containers. 2053 * 2054 * _CFG_PSLOT defines which PCODEs are eligible. Merely being present in 2055 * oEMU.component.IO is not sufficient: the container must expose 2056 * id.PCODE, and that PCODE must occur in _CFG_PSLOT. 2057 * 2058 * The constructor is retained directly. No parallel registry and no 2059 * globalThis constructor lookup are required. 2060 */ 2061 this.scanPeripheralContainers = function() 2062 { 2063 var discovered = {}; 2064 2065 if(typeof(_CFG_PSLOT)=="undefined" || 2066 !oEMU.component || 2067 !oEMU.component.IO) 2068 return discovered; 2069 2070 for(var coID in oEMU.component.IO) 2071 { 2072 var container = oEMU.component.IO[coID]; 2073 var PCODE = container?.id?.PCODE; 2074 2075 if(!PCODE || !_CFG_PSLOT[PCODE]) continue; 2076 if(typeof(container.constructor)!="function") continue; 2077 if(container.constructor===Object) continue; 2078 2079 var info = Object.assign({},container.id,{ 2080 "PCODE":PCODE 2081 ,"coID":coID 2082 ,"ctor":container.constructor 2083 }); 2084 2085 if(info.description===undefined) 2086 info.description = _CFG_PSLOT[PCODE].NAME; 2087 2088 discovered[PCODE] = info; 2089 } 2090 2091 return discovered; 2092 } 2093 function extract_slotrange_mask(str) 2094 { 2095 var mask = new Uint16Array(2); 2096 mask[0] = 0; mask[1] = 0; 2097 2098 var arr = str.split(","); 2099 for(var i=0;i<arr.length;i++) 2100 { 2101 var n = Number( arr[i].replace(RegExp("\\*","g"),"") ); 2102 if(isNaN(n)==false) mask[0] |= 1<<n 2103 if(arr[i].indexOf("*")>=0) mask[1] |= 1<<n 2104 } 2105 return mask; // [0]:compatible slotrange for peripheral [1]:pre-installed slots with peripheral 2106 } 2107 2108 function extract_slotrange(str) 2109 { 2110 var slotPut = [], slotFit = []; 2111 var arr = str.split(","); 2112 for(var i=0;i<arr.length;i++) 2113 { 2114 var n = Number( arr[i].replace(RegExp("\\*","g"),"") ); 2115 if(isNaN(n)==false) 2116 { 2117 slotFit.push( n ); 2118 } 2119 if(arr[i].indexOf("*")>=0) 2120 { 2121 slotPut.push( n ); 2122 } 2123 } 2124 return {"slotPut":slotPut, "slotFit":slotFit} 2125 } 2126 2127 function extract_slotRef(str,PCODE,ref) 2128 { 2129 if(ref.slotFit===undefined) ref.slotFit = []; 2130 if(ref.slotMap===undefined) ref.slotMap = []; 2131 var arr = str.split(","); 2132 for(var i=0;i<arr.length;i++) 2133 { 2134 var n = slotID2n( arr[i].replace(RegExp("\\*","g"),"") ); 2135 2136 if(ref.slotFit[n]===undefined) { ref.slotFit[n] = [] }; 2137 ref.slotFit[n].push(PCODE); 2138 2139 if(arr[i].indexOf("*")>=0) 2140 { 2141 if(ref.slotMap[n]===undefined) { ref.slotMap[n] = [] }; 2142 ref.slotMap[n].push(PCODE); 2143 } 2144 } 2145 return {"slotMap":ref.slotMap, "slotFit":ref.slotFit} 2146 } 2147 2148 function slotID2n(slotID) 2149 { 2150 // previously slotID == "B" 2151 return slotID == "H" ? 0 : Number(slotID) + 1; 2152 } 2153 2154 this.slot2ID = function(slotN) 2155 { 2156 // previously slotID == "B" 2157 return slotN == 0 ? "H" : Number(slotN) - 1; 2158 } 2159 2160 function slotN2name(slotN) // name = display name of a slot 2161 { 2162 return slotN == 0 ? "board" : ("PR#"+(slotN-1)); 2163 } 2164 2165 function slotName2n(slotName) 2166 { 2167 return slotName=="board" ? 0 : Number(slotName.slice(-1))+1 2168 } 2169 2170 function peripheralPCODE(p) 2171 { 2172 return p ? (p.id && p.id.PCODE ? p.id.PCODE : (p.PCODE || "")) : ""; 2173 } 2174 2175 function peripheralIcon(p) 2176 { 2177 return p ? (p.id && p.id.icon ? p.id.icon : (p.icon || "fa fa-cube")) : "fa fa-cube";
2178 } 2179 2180 function peripheralCoID(p) 2181 { 2182 return p ? (p.id && p.id.coID ? p.id.coID : (p.coID || (p.constructor ? p.constructor.name : ""))) : ""; 2183 } 2184 2185 function peripheralDescription(p) 2186 { 2187 return p ? (p.id && p.id.description ? p.id.description : (p.description || "")) : ""; 2188 } 2189 2190 function peripheralRange(p,rangeName) 2191 { 2192 if(!p) return undefined; 2193 if(p.mount && p.mount.ranges && p.mount.ranges[rangeName]) return p.mount.ranges[rangeName]; 2194 return p[rangeName]; // legacy fallback 2195 } 2196 2197 this.HASH2obj = function(HASH) 2198 { 2199 for(var o in this.slots) 2200 { 2201 if(this.slots[o].peripheral?.mount.hash == HASH) 2202 return this.slots[o].peripheral; // we can have multiple matches, since we do not specify slotN 2203 } 2204 return null; 2205 } 2206 2207 this.SLOT2obj = function(slotN) 2208 { 2209 slotN = Number(slotN); 2210 2211 if( 2212 !Number.isInteger(slotN) || 2213 !this.slots[slotN] || 2214 !this.slots[slotN].peripheral 2215 ) 2216 return null; 2217 2218 return this.slots[slotN].peripheral; 2219 } 2220 2221 this.DeviceName2obj = function(str) 2222 { 2223 2224 } 2225 2226 this.deviceID2obj = function(str,slotN) 2227 { 2228 if(typeof str !== "string") return null; 2229 2230 var DCODE = str.trim().toUpperCase(); 2231 if(!DCODE) return null; 2232 2233 var slotNumbers = []; 2234 var requestedSlotN = Number(slotN); 2235 2236 if(slotN!==undefined && slotN!==null && Number.isInteger(requestedSlotN)) 2237 slotNumbers.push(requestedSlotN); 2238 else 2239 { 2240 for(var mountedSlotN in this.slots) 2241 if(this.slots[mountedSlotN] && this.slots[mountedSlotN].peripheral) 2242 slotNumbers.push(Number(mountedSlotN)); 2243 } 2244 2245 for(var s=0;s<slotNumbers.length;s++) 2246 { 2247 var resolvedSlotN = slotNumbers[s]; 2248 var owner = this.SLOT2obj(resolvedSlotN); 2249 var devices = owner && Array.isArray(owner.devices) ? owner.devices : []; 2250 2251 for(var d=0;d<devices.length;d++) 2252 { 2253 var device = devices[d]; 2254 var id = device && device.id ? device.id : {};
2255 var candidate = String(id.DCODE || "").toUpperCase(); 2256 2257 if(candidate!=DCODE) continue; 2258 2259 var deviceN = Number(id.deviceN); 2260 if(!Number.isInteger(deviceN)) deviceN = d; 2261 2262 return { 2263 "slotN":resolvedSlotN 2264 ,"slotID":this.slot2ID(resolvedSlotN) 2265 ,"periID":peripheralPCODE(owner) 2266 ,"DCODE":candidate 2267 ,"deviceID":candidate 2268 ,"deviceN":deviceN 2269 }; 2270 } 2271 } 2272 2273 return null; 2274 }; 2275 2276 this.obj2deviceID = function(obj) 2277 { 2278 if(obj == null) return null; 2279 2280 if(typeof obj === "string") 2281 return obj.trim().toUpperCase() || null; 2282 2283 var DCODE = 2284 obj.id?.DCODE 2285 || obj.DCODE 2286 || obj.deviceID 2287 || obj.device?.id?.DCODE; 2288 2289 if(typeof DCODE === "string") 2290 return DCODE.trim().toUpperCase() || null; 2291 2292 return null; 2293 }; 2294 2295 this.deviceN2ID = function(n,PCODE,slotN) 2296 { 2297 var owner = Number.isInteger(Number(slotN)) 2298 ? this.SLOT2obj(Number(slotN)) 2299 : this.PCODE2obj(PCODE)[0]; 2300 if(PCODE && peripheralPCODE(owner)!==PCODE) return null; 2301 2302 var devices = owner && Array.isArray(owner.devices) ? owner.devices : []; 2303 var deviceN = Number(n); 2304 2305 for(var i=0;i<devices.length;i++) 2306 { 2307 var ordinal = Number(devices[i]?.id?.deviceN); 2308 if(!Number.isInteger(ordinal)) ordinal = i; 2309 if(ordinal===deviceN) return this.obj2deviceID(devices[i]); 2310 } 2311 2312 return null; 2313 } 2314 2315 this.deviceID2N = function(str,PCODE,slotN) 2316 { 2317 var ref = this.deviceID2obj(str,slotN); 2318 return ref && (!PCODE || ref.periID===PCODE) ? ref.deviceN : undefined; 2319 } 2320 2321 this.config_slotAvail = function(cfg) // HOW MANY SLOTS CAN WE FILL? 2322 { 2323 var model = typeof(EMU_system_get)=="function" ? EMU_system_get() : "A2P"; 2324 if(cfg===undefined) { var cfg = "[0-7] [0-7]"; console.warn("CONFIG file is not available (missing _CFG_SYSCODE)"); } 2325 var str = cfg[model]?.Slotslogphy; 2326 var a = parseSlotAvail(str); 2327 2328 // dataset completions 2329 for(var i=0;i<a.logSlots.length;i++) a.logSlots[i] = "PR#" + a.logSlots[i]; 2330 for(var i in a.lockSlots) a.lockSlots[i] = "PR#" + a.lockSlots[i]; 2331 2332 a.logSlots.unshift("board"); a.lockSlots["board"] = true; 2333 return a; 2334 } 2335 2336 function parseSlotAvail(s) 2337 { 2338 if(s===undefined) return {}; 2339 let a=[...s.matchAll(/\[(\d+)-(\d+)\]/g)].map(m=> 2340 Array.from({length:m[2]-m[1]+1},(_,i)=>+m[1]+i) 2341 ); 2342 return { 2343 logSlots:a[0], 2344 phySlots:a[1], 2345 lockSlots:Object.fromEntries(a[0].filter(n=>!a[1].includes(n)).map(n=>[n,true])), 2346 logSlots_n:a[0].length, 2347 phySlots_n:a[1].length 2348 }; 2349 } 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 // SLOT MAPPING 2363 //var SLOT_MAP = new Uint16Array(8<<3); // SLOT ADDRESS MAPPING 2364 //var SLOT_NAME = new Array(8); // 8 SLOTS, 6 CHARACTERS PER SLOT NAME 2365 var SLOT_IDX = new Array(8); 2366 var SLOT_REG = []; 2367 2368 var model = typeof(EMU_system_get)=="function" ? EMU_system_get() : "A2P"; 2369 2370 var slot_count = 0; 2371 if(typeof(_CFG_SYSCODE)!="undefined") 2372 { 2373 var slotAvail = this.config_slotAvail(_CFG_SYSCODE); 2374 // example: slotAvail={"logSlots":["board","PR#0","PR#1","PR#2","PR#3","PR#4","PR#5","PR#6","PR#7"],"phySlots":[0,1,2,3,4,5,6,7],"lockSlots":{"board":true},"logSlots_n":8,"phySlots_n":8} 2375 var slot_count = slotAvail.logSlots_n; 2376 } 2377 2378 var slotR = {}; // TODO: remove as this is now in CONFIG 2379 2380 if(typeof(_CFG_PSLOT)!="undefined") // DO WE HAVE A CONFIGURATION FILE FOR OUR PERIPHERALS? 2381 { 2382 // LOAD ALL PERIPHERALS FROM CONFIGURATION 2383 for(var PCODE in _CFG_PSLOT) 2384 { 2385 var srange = _CFG_PSLOT[PCODE].SLOTrange.split(","); 2386 //var slotrange_mask = extract_slotrange_mask(_CFG_PSLOT[PCODE].SLOTrange); // [0]:compatible slotrange for peripheral [1]:pre-installed slots with peripheral 2387 slotR = extract_slotRef(_CFG_PSLOT[PCODE].SLOTrange,PCODE,slotR); 2388 } 2389 2390 //slotR = {"slotMap":[["A2BO"],["MS16K"],null,null,["VIDEX"],["MOCK"],null,["DISKII"]] 2391 // ,"slotFit":[["A2BO"],["MS16K"],["DISKII","MOCK"],["DISKII","MOCK"],["DISKII","VIDEX","MOCK"],["DISKII","MOCK"],["DISKII","MOCK"],["DISKII","MOCK"],["DISKII","MOCK"]]} 2392 2393 } 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403// ______ __ _ ___ _ 2404// .' ____ \ [ | / |_ .' ..](_) 2405// | (___ \_| | | .--. `| |-' .---. .--. _ .--. _| |_ __ .--./) 2406// _.____`. | |/ .'`\ \| | / /'`\]/ .'`\ \[ `.-. |'-| |-'[ | / /'`\; 2407// | \____) | | || \__. || |, | \__. | \__. | | | | | | | | | \ \._// 2408// \______.'[___]'.__.' \__/ '.___.' '.__.' [___||__][___] [___].',__` 2409// ( ( __)) 2410 2411 this.onSlotAdd = function(ctx, ev) 2412 { 2413 var anchor = ev.target.closest(".slot-anchor.empty"); 2414 if (!anchor || !ctx.host.contains(anchor)) return; 2415 2416 var slotTitle = anchor.dataset.slotId; 2417 this.slotPicker_popup(ctx, slotTitle); 2418 }; 2419 2420 this.onSlotMove = function(ctx, ev, fromSlotId, toSlotId) 2421 { 2422 var parent = ev.currentTarget.parentElement.parentElement.parentElement; 2423 var periID = parent.children[0].id; 2424 2425 //console.log(JSON.stringify(ctx.slots)); 2426 //alert("Move " + ctx.hostId + " peripheral "+periID+" slot " + fromSlotId + "->" + toSlotId); 2427 //alert("onSlotMove checkpoint: this.slots["+toSlotId+"] = "+JSON.stringify(this.slots[ slotName2n(toSlotId) ])); 2428 }; 2429 2430 this.slotsRender = function(elid, cfg) 2431 { 2432 if (!elid) return; 2433 2434 if (!this.slot_ctx[elid]) 2435 { 2436 this.slot_ctx[elid] = { 2437 hostId: elid, 2438 host: null, 2439 slots: null, 2440 clickBound: false, 2441 pointerDrag: null 2442 }; 2443 } 2444 2445 var ctx = this.slot_ctx[elid]; 2446 if (cfg !== undefined) ctx.slots = cfg; 2447 if (!ctx.slots) ctx.slots = this.slots; 2448 2449 ctx.host = document.getElementById(elid); 2450 if (!ctx.host) return; 2451 2452 function getSlotById(Cfg, slotTitle) { return Cfg.find(function(slot) { return slot.slotTitle === slotTitle; }) } 2453 2454 function renderGhost(slot) 2455 { 2456 var iconClass = slot && slot.peripheral ? peripheralIcon(slot.peripheral) : "fa fa-circle"; 2457 2458 return ` 2459 <button class="appbut ghost-cog" type="button" tabindex="-1" aria-hidden="true"> 2460 <i class="${iconClass}"></i> 2461 </button>
2462 <span class="ghost-handle" aria-hidden="true"> 2463 <i class="fa fa-ellipsis-h"></i> 2464 <i class="fa fa-ellipsis-h"></i> 2465 </span> 2466 `; 2467 } 2468 2469 function renderAnchor(slot) 2470 { 2471 var anchorId = "d_slot_" + ctx.hostId + "_" + slot.slotTitle.replace(/[^a-zA-Z0-9_-]/g, "_"); 2472 2473 if (slot.peripheral) 2474 { 2475 return ` 2476 <div class="slot-anchor occupied ${slot.lock ? "locked" : "movable"}" 2477 id="${anchorId}" 2478 data-slot-id="${slot.slotTitle}" 2479 data-host-id="${ctx.hostId}"> 2480 <div class="peripheral-card"> 2481 ${slot.lock ? "" : ` 2482 <span class="drag-handle" title="Move ${peripheralPCODE(slot.peripheral)}"> 2483 <i class="fa fa-ellipsis-h dots dots1"></i> 2484 <i class="fa fa-ellipsis-h dots"></i> 2485 </span> 2486 `} 2487 </div> 2488 </div> 2489 `; 2490 } 2491 2492 return ` 2493 <div class="slot-anchor empty" 2494 id="${anchorId}" 2495 data-slot-id="${slot.slotTitle}" 2496 data-host-id="${ctx.hostId}"> 2497 <button class="slot-add" type="button" title="Add peripheral to ${slot.slotTitle}"> 2498 <i class="fa fa-plus dots dots1"></i> 2499 </button> 2500 </div> 2501 `; 2502 } 2503
2504 function renderSlot(slot) 2505 { 2506 return ` 2507 <div class="appbox slotbox slot" 2508 data-slot-id="${slot.slotTitle}" 2509 data-host-id="${ctx.hostId}"> 2510 <div class="slot-label">${slot.slotTitle}</div> 2511 <div class="sloticons"> 2512 ${slot.peripheral 2513 ? `<button id="${peripheralPCODE(slot.peripheral)}" class="appbut cogbtn" type="button" title="configure ${peripheralPCODE(slot.peripheral)} / ${peripheralCoID(slot.peripheral)}()"><i class="${peripheralIcon(slot.peripheral)}"></i></button>` 2514 : ""} 2515 ${renderAnchor(slot)} 2516 </div> 2517 </div> 2518 `; 2519 } 2520 2521 function buildDragGhost(slot) 2522 { 2523 var ghost = document.createElement("div"); 2524 ghost.className = "drag-ghost"; 2525 ghost.innerHTML = renderGhost(slot); 2526 document.body.appendChild(ghost); 2527 return ghost; 2528 } 2529 2530 function positionDragGhost(x, y) 2531 { 2532 if (!ctx.pointerDrag || !ctx.pointerDrag.ghostEl) return; 2533 ctx.pointerDrag.ghostEl.style.left = x + "px"; 2534 ctx.pointerDrag.ghostEl.style.top = y + "px"; 2535 } 2536 2537 function updatePointerHotTarget(x, y) 2538 { 2539 if (!ctx.pointerDrag) return; 2540 var el = document.elementFromPoint(x, y); 2541 var nextHot = el ? el.closest('[data-host-id="' + ctx.hostId + '"].slot-anchor.empty') : null; 2542 if (ctx.pointerDrag.hotTarget === nextHot) return; 2543 if (ctx.pointerDrag.hotTarget) ctx.pointerDrag.hotTarget.classList.remove("drop-ok"); 2544 ctx.pointerDrag.hotTarget = nextHot; 2545 if (ctx.pointerDrag.hotTarget) ctx.pointerDrag.hotTarget.classList.add("drop-ok"); 2546 } 2547 2548 function cleanupPointerDrag() 2549 { 2550 if (!ctx.pointerDrag) return; 2551 var handleEl = ctx.pointerDrag.handleEl; 2552 var pointerId = ctx.pointerDrag.pointerId; 2553 if (ctx.pointerDrag.hotTarget) ctx.pointerDrag.hotTarget.classList.remove("drop-ok"); 2554 if (ctx.pointerDrag.ghostEl) ctx.pointerDrag.ghostEl.remove(); 2555 if (handleEl) 2556 { 2557 try { if (handleEl.hasPointerCapture(pointerId)) handleEl.releasePointerCapture(pointerId) } 2558 catch (e) {} 2559 2560 handleEl.removeEventListener("pointermove", onPointerDragMove); 2561 handleEl.removeEventListener("pointerup", onPointerDragEnd); 2562 handleEl.removeEventListener("pointercancel", onPointerDragCancel); 2563 } 2564 2565 document.body.classList.remove("dragging"); 2566 ctx.host.querySelectorAll(".slot.drag-source").forEach(function(el) 2567 { 2568 el.classList.remove("drag-source"); 2569 }); 2570 2571 ctx.pointerDrag = null; 2572 } 2573 2574 function onPointerDragStart(emui, host, ev) 2575 { 2576 if (ev.button != null && ev.button !== 0) return; 2577 2578 ev.preventDefault(); 2579 ev.stopPropagation(); 2580 2581 var slotEl = ev.currentTarget.closest(".slot"); 2582 if (!slotEl) return; 2583 2584 var fromSlotId = slotEl.dataset.slotId; 2585 var fromSlot = getSlotById(ctx.slots, fromSlotId); 2586 if (!fromSlot || !fromSlot.peripheral) return; 2587 2588 ctx.pointerDrag = { 2589 emui: emui, 2590 host: host, 2591 pointerId: ev.pointerId, 2592 fromSlotId: fromSlotId, 2593 handleEl: ev.currentTarget, 2594 hotTarget: null, 2595 ghostEl: buildDragGhost(fromSlot) 2596 }; 2597 2598 document.body.classList.add("dragging"); 2599 slotEl.classList.add("drag-source"); 2600 2601 ev.currentTarget.setPointerCapture(ev.pointerId); 2602 ev.currentTarget.addEventListener("pointermove", onPointerDragMove); 2603 ev.currentTarget.addEventListener("pointerup", onPointerDragEnd); 2604 ev.currentTarget.addEventListener("pointercancel", onPointerDragCancel); 2605 2606 positionDragGhost(ev.clientX, ev.clientY); 2607 updatePointerHotTarget(ev.clientX, ev.clientY); 2608 } 2609 2610 function onPointerDragMove(ev) 2611 { 2612 if (!ctx.pointerDrag || ev.pointerId !== ctx.pointerDrag.pointerId) return; 2613 2614 ev.preventDefault(); 2615 positionDragGhost(ev.clientX, ev.clientY); 2616 updatePointerHotTarget(ev.clientX, ev.clientY); 2617 } 2618 2619 function onPointerDragEnd(ev) 2620 { 2621 if (!ctx.pointerDrag || ev.pointerId !== ctx.pointerDrag.pointerId) return; 2622 ev.preventDefault(); 2623 var fromSlotId = ctx.pointerDrag.fromSlotId; 2624 var toSlotId = ctx.pointerDrag.hotTarget ? ctx.pointerDrag.hotTarget.dataset.slotId : null; 2625 var emui = ctx.pointerDrag.emui; 2626 cleanupPointerDrag(); 2627 if (toSlotId && slotMove(ctx.slots, fromSlotId, toSlotId)) 2628 { 2629 emui.slotsRender(ctx.hostId); 2630 console.log("Move peripheral from->to", ctx.hostId, fromSlotId, toSlotId); 2631 emui.onSlotMove(ctx, ev, fromSlotId, toSlotId); 2632 } 2633 2634 function slotMove(slots, fromSlotId, toSlotId) 2635 { 2636 if (!slots || 2637 !fromSlotId || 2638 !toSlotId || 2639 fromSlotId === toSlotId) 2640 return false;
2641 2642 var from = getSlotById(slots, fromSlotId); 2643 var to = getSlotById(slots, toSlotId); 2644 2645 if (!from || 2646 !to || 2647 from.lock || 2648 !from.peripheral || 2649 to.peripheral) 2650 return false; 2651 2652 var peripheral = from.peripheral; 2653 var pcode = peripheralPCODE(peripheral); 2654 var discovered = emui.scanPeripheralContainers(); 2655 var pinfo = discovered[pcode] || null; 2656 var cinfo = typeof(_CFG_PSLOT)!="undefined" ? _CFG_PSLOT[pcode] : null; 2657 var toSlotN = slotName2n(toSlotId); 2658 var mounted = emui.mount( 2659 cinfo 2660 ,pinfo 2661 ,toSlotN 2662 ,slotR.slotFit ? slotR.slotFit[toSlotN] : [] 2663 ,peripheral 2664 2665 ); 2666 if(!mounted || !mounted.pObj) return false; 2667 2668 for(var key in mounted.sInfo) to[key] = mounted.sInfo[key]; 2669 to.peripheral = mounted.pObj; 2670 2671 var model = typeof(EMU_system_get)=="function" ? EMU_system_get() : "A2P"; 2672 emui.provisionPeripheral(mounted.pObj,model); 2673 refillEmptyIOActions(); 2674 2675 delete from.peripheral; 2676 return true; 2677 } 2678 } 2679 2680 function onPointerDragCancel(ev) { if (ctx.pointerDrag && ev.pointerId == ctx.pointerDrag.pointerId) cleanupPointerDrag() } 2681 function onClick(emui, ctx, ev) { emui.onSlotClick(ctx, ev) } 2682 function onHostClick(emui, ctx, ev) 2683 { 2684 var addBtn = ev.target.closest(".slot-add"); 2685 if (addBtn && ctx.host.contains(addBtn)) emui.onSlotAdd(ctx, ev); 2686 } 2687 2688 function wireEvents(emui, ctx) 2689 { 2690 ctx.host.querySelectorAll(".drag-handle").forEach(function(el) 2691 { 2692 el.addEventListener("pointerdown", function(ev) { onPointerDragStart(emui, ctx, ev) }); 2693 }); 2694 2695 ctx.host.querySelectorAll(".appbut").forEach(function(el) 2696 { 2697 el.addEventListener("click", function(ev) { onClick(emui, ctx, ev) }); 2698 }); 2699 2700 if (!ctx.clickBound) 2701 { 2702 ctx.host.addEventListener("click", function(ev) { onHostClick(emui, ctx, ev) }); 2703 ctx.clickBound = true; 2704 } 2705 } 2706 ctx.host.innerHTML = (ctx.slots || []).map(renderSlot).join(""); 2707 2708 wireEvents(this, ctx); 2709 }; 2710 2711 this.onSlotClick = function(ctx, ev) 2712 { 2713 var parent = ev.currentTarget.closest(".slot"); 2714 var periID = ev.currentTarget.id; 2715 var slotID = parent ? parent.dataset.slotId : ""; 2716 var hostId = ctx.hostId; 2717 2718 this.slotConfig_detail(slotID); 2719 2720 2721 //oCOM.POPUP.toggle("slotConfig_popup"); 2722 2723 console.log(JSON.stringify(ctx.slots)); 2724 }; 2725 2726 2727 this.slotConfig = function(arg) 2728 { 2729 if(arg.active==false) document.getElementById(arg.id).innerHTML = ""; 2730 2731 //var ss = "<div class=\"appbut\" onclick=\"oCOM.POPUP.toggle('"+wrapper_id+"');\" style=\"text-align:center;float:right;\">x</div>" 2732 var s = "document.getElementById('"+arg.id+"').innerHTML='"+arg.id+"';" 2733 if(document.getElementById(arg.id)!=null) 2734 document.getElementById(arg.id).innerHTML = "<button class=appbut onclick=\"apple2plus.hwObj().io.slotConfig_detail('"+arg.id+"')\" style=\"margin-left:0px\"><i class=\""+arg.icon+"\"></i></button>" 2735 } 2736 2737 this.slotConfig_refresh = function(slotN) 2738 { 2739 slotN=Number(slotN); 2740 var slot=this.slots[slotN] || {}; 2741 var popup=document.getElementById("slotConfig_popup"); 2742 if(!popup) return false; 2743 2744 var close="<div class=\"appbut\" onclick=\"oCOM.POPUP.toggle('slotConfig_popup');apple2plus.hwObj().io.deviceConfig_close();\" style=\"text-align:center;float:right;\">x</div>"; 2745 var html=close+slotConfigDetail_html(slot); 2746 if(slotN==0) 2747 { 2748 html += "<div style='overflow-y:scroll;height:350px'>"; 2749 var model=typeof(EMU_system_get)=="function" ? EMU_system_get() : "A2P"; 2750 var board=this.SLOT2obj(0); 2751 html += "<div class='appbox' style='float:none;'>"+(board && typeof(board.boardIO_html)=="function" ? board.boardIO_html(model) : "")+"</div>"; 2752 html += "</div>"; 2753 } 2754 popup.innerHTML=html; 2755 return true; 2756 }; 2757 2758 this.slotConfig_detail = function(slotName) 2759 { 2760 var n=slotName2n(slotName); 2761 this.slotConfig_refresh(n); 2762 this.deviceConfig_close(); 2763 oCOM.POPUP.toggle("slotConfig_popup"); 2764 } 2765 2766 this.slotConfig_download = function(slotN) 2767 { 2768 slotN = Number(slotN); 2769 var slot = this.slots[slotN]; 2770 if(!slot) return false;
2771 2772 try 2773 { 2774 var pcode = peripheralPCODE(slot.peripheral) || "slot"; 2775 var slotName = slotN2name(slotN).replace("#",""); 2776 var fileName = (pcode+"_"+slotName).replace(/[^A-Za-z0-9_.-]/g,"_")+".json"; 2777 var json = JSON.stringify(slot,null,2); 2778 oCOM.Download(fileName,new TextEncoder("utf-8").encode(json)); 2779 return true; 2780 } 2781 catch(e) 2782 { 2783 console.error("Slot JSON download failed",e); 2784 return false; 2785 } 2786 } 2787 2788 this.slotConfig_eject = function(slotN) 2789 { 2790 slotN = Number(slotN); 2791 var slot = this.slots[slotN]; 2792 2793 if(!Number.isInteger(slotN) || !slot || slot.lock || !slot.peripheral) 2794 return false; 2795 2796 var previous = document.getElementById("slotConfig_eject_confirm"); 2797 if(previous) previous.remove(); 2798 2799 var io = this; 2800 var peripheral = slot.peripheral; 2801 var dialog = document.createElement("dialog"); 2802 dialog.id = "slotConfig_eject_confirm"; 2803 dialog.className = "appbox"; 2804 dialog.style.cssText = "max-width:360px;text-align:left;padding:12px"; 2805 2806 var question = document.createElement("div"); 2807 question.style.marginBottom = "12px"; 2808 question.textContent = "Eject " 2809 + peripheralPCODE(peripheral) 2810 + " from " 2811 + slotN2name(slotN) 2812 + "?"; 2813 2814 var note = document.createElement("div"); 2815 note.style.cssText = "font-size:11px;margin-bottom:12px"; 2816 note.textContent = "The peripheral and its active I/O mappings will be removed."; 2817 2818 var buttons = document.createElement("div"); 2819 buttons.style.textAlign = "right"; 2820 2821 var noButton = document.createElement("button"); 2822 noButton.type = "button"; 2823 noButton.className = "appbut"; 2824 noButton.textContent = "No"; 2825 2826 var yesButton = document.createElement("button"); 2827 yesButton.type = "button"; 2828 yesButton.className = "appbut"; 2829 yesButton.textContent = "Yes"; 2830 2831 function eraseDialog() 2832 { 2833 if(dialog.open && typeof(dialog.close)=="function") 2834 dialog.close(); 2835 dialog.remove(); 2836 } 2837 2838 noButton.onclick = eraseDialog; 2839 dialog.addEventListener("cancel",function(ev) 2840 { 2841 ev.preventDefault(); 2842 eraseDialog(); 2843 }); 2844 2845 yesButton.onclick = function() 2846 { 2847 var removed = io.unmount(slotN); 2848 eraseDialog(); 2849 if(!removed) return; 2850 2851 var detail = document.getElementById("slotConfig_popup"); 2852 if(detail) 2853 { 2854 detail.innerHTML = ""; 2855 oCOM.POPUP.off("slotConfig_popup"); 2856 } 2857 2858 for(var hostId in io.slot_ctx) 2859 io.slotsRender(hostId); 2860 2861 if(typeof(io.refreshDeviceToolboxes)=="function") 2862 io.refreshDeviceToolboxes({"id":"devices"}); 2863 }; 2864 2865 buttons.appendChild(noButton); 2866 buttons.appendChild(yesButton); 2867 dialog.appendChild(question); 2868 dialog.appendChild(note); 2869 dialog.appendChild(buttons); 2870 document.body.appendChild(dialog); 2871 2872 if(typeof(dialog.showModal)=="function") dialog.showModal(); 2873 else dialog.setAttribute("open",""); 2874 return true; 2875 } 2876 2877 function slotConfigDetail_html(slot) 2878 { 2879 var peripheral = slot && slot.peripheral; 2880 if(!peripheral) 2881 return "<div class='appbox' style='margin-top:8px;padding:8px'>No peripheral mounted.</div>"; 2882 2883 var rows = []; 2884 var rangeNames = ["HostIO","SlotIO","SlotROM","HostROM"]; 2885 2886 // Static mappings declared by peripheral.action and peripheral.mount.ranges. 2887 for(var i=0;i<rangeNames.length;i++) 2888 { 2889 var rangeName = rangeNames[i]; 2890 var range = peripheralRange(peripheral,rangeName); 2891 if(!range) continue; 2892 2893 var action = peripheral.action && peripheral.action[rangeName] 2894 ? peripheral.action[rangeName] 2895 : {}; 2896 var operations = []; 2897 2898 if(action.RD && typeof(action.RD.callback)=="function") 2899 operations.push({"op":"RD","title":"RD -> "+callbackLabel(action.RD.callback)}); 2900 if(action.WR && typeof(action.WR.callback)=="function") 2901 operations.push({"op":"WR","title":"WR -> "+callbackLabel(action.WR.callback)}); 2902 2903 rows.push({ 2904 "space":rangeName 2905 ,"cpuFrom":range.from+ioBase() 2906 ,"cpuTo":range.to+ioBase() 2907 ,"offsetFrom":range.from 2908 ,"offsetTo":range.to 2909 ,"operations":operations 2910 }); 2911 } 2912 2913 // Dynamic CPU mappings registered through MEMORY_MAP. 2914 var dynamicMappings = peripheral.mount && Array.isArray(peripheral.mount.mappings) 2915 ? peripheral.mount.mappings 2916 : []; 2917 2918 for(var j=0;j<dynamicMappings.length;j++) 2919 { 2920 var mapping = dynamicMappings[j]; 2921 var space = mapping.space || "MEMORY MAPPING"; 2922 var operation = { 2923 "op":mapping.op 2924 ,"title":mappingTooltip(mapping) 2925 ,"enabled":mapping.enabled!==false 2926 }; 2927 var row = rows.find(function(candidate) 2928 { 2929 return candidate.space===space && 2930 candidate.cpuFrom===mapping.from && 2931 candidate.cpuTo===mapping.to && 2932 candidate.offsetFrom===undefined && 2933 candidate.offsetTo===undefined; 2934 }); 2935 2936 /* Keep RD/WR as separate evidence, but combine their display row. */ 2937 if(row) 2938 row.operations.push(operation); 2939 else 2940 rows.push({ 2941 "space":space 2942 ,"cpuFrom":mapping.from 2943 ,"cpuTo":mapping.to 2944 ,"operations":[operation] 2945 }); 2946 2947 } 2948 2949 var body = ""; 2950 for(var r=0;r<rows.length;r++) body += mappingRow_html(rows[r]); 2951 if(!body) body = "<tr><td colspan='4' style='padding:8px'>No mappings registered.</td></tr>"; 2952 2953 var slotN = Number(peripheral.mount.slotN); 2954 var ejectButton = "<button class=\"appbut\" type=\"button\"" 2955 + (slot.lock 2956 ? " disabled title=\"This peripheral is locked\"" 2957 : " title=\"Eject peripheral\"" 2958 + " onclick=\"event.stopPropagation();apple2plus.hwObj().io.slotConfig_eject("+slotN+")\"") 2959 + ">" 2960 + "<i class=\"fa fa-eject\"></i>" 2961 + "</button>"; 2962 return "<div class='appbox' style='float:none;width:520px;max-width:80vw;overflow:auto;margin-top:8px;padding:8px'>" 2963 + "<div><b>"+oCOM.escapeHTML(peripheralPCODE(peripheral))+"</b>" 2964 + (peripheralDescription(peripheral) ? " — "+oCOM.escapeHTML(peripheralDescription(peripheral)) : "") 2965 + " — " + slotN2name(slot.peripheral.mount.slotN) 2966 + " <button class=\"appbut\" type=\"button\"" 2967 + " title=\"Download slot JSON\"" 2968 + " onclick=\"event.stopPropagation();apple2plus.hwObj().io.slotConfig_download("+slotN+")\">" 2969 + "<i class=\"fa fa-cloud-download-alt\"></i></button>" 2970 + " "+ejectButton 2971 + "</div>" 2972 2973 + "<table style='width:100%;border-collapse:collapse;margin-top:8px;text-align:left'>" 2974 + "<thead><tr style='border-bottom:1px solid #888'>" 2975 + "<th style='padding:4px 6px'>Space</th>" 2976 + "<th style='padding:4px 6px'>CPU address</th>" 2977 + "<th style='padding:4px 6px'>I/O offset</th>" 2978 + "<th style='padding:4px 6px'>R/W</th>" 2979 + "</tr></thead>" 2980 + "<tbody>"+body+"</tbody>" 2981 + "</table>" 2982 + slotDeviceTable_html(peripheral) 2983 + "</div>" 2984 } 2985 2986 function mappingRow_html(row) 2987 { 2988 return "" 2989 + "<tr>" 2990 + "<td style='padding:4px 6px'>"+oCOM.escapeHTML(row.space)+"</td>" 2991 + "<td style='padding:4px 6px'>"+mappingRange_html(row.cpuFrom,row.cpuTo)+"</td>" 2992 + "<td style='padding:4px 6px'>"+mappingRange_html(row.offsetFrom,row.offsetTo)+"</td>" 2993 + "<td style='padding:4px 6px'>"+mappingOperations_html(row.operations)+"</td>" 2994 + "</tr>"; 2995 } 2996 2997 function mappingRange_html(from,to,digits) 2998 { 2999 if(from===undefined || to===undefined) return "—"; 3000 var range = digits===undefined 3001 ? fmtRange(from,to,0) 3002 : "$"+oCOM.getHexMulti(from,digits)+"-$"+oCOM.getHexMulti(to,digits); 3003 3004 return "<div class=\"appbut skinny\">"+range+"</div>"; 3005 } 3006 3007 function mappingOperations_html(operations) 3008 { 3009 if(!operations || !operations.length) return "—"; 3010 3011 var html = "<div style='display:flex;gap:3px;white-space:nowrap'>"; 3012 for(var i=0;i<operations.length;i++) 3013 { 3014 var operation = operations[i]; 3015 html += "<div class=\"appbut skinny\"" 3016 + (operation.title ? " title=\""+oCOM.escapeHTML(operation.title)+"\"" : "") 3017 + (operation.enabled===false ? " style=\"opacity:.55\"" : "") 3018 + ">"+oCOM.escapeHTML(operation.op)+"</div>"; 3019 } 3020 return html+"</div>"; 3021 } 3022 3023 function mappingTooltip(mapping) 3024 { 3025 var parts = []; 3026 if(mapping.target) parts.push(mapping.op+" -> "+mapping.target); 3027 if(mapping.condition) parts.push(mapping.condition); 3028 if(mapping.enabled===false) parts.push("disabled by current soft-switch state"); 3029 if(mapping.source) parts.push(mapping.source); 3030 return parts.join("; "); 3031 } 3032 3033 function deviceJSONClone(value,seen) 3034 { 3035 if(value===null || value===undefined) return value;
3036 if(typeof(value)=="string" || typeof(value)=="number" || typeof(value)=="boolean") return value; 3037 if(typeof(value)=="function" || typeof(value)=="symbol") return undefined; 3038 3039 if(typeof(ArrayBuffer)!="undefined" && ArrayBuffer.isView && ArrayBuffer.isView(value)) 3040 return Array.from(value); 3041 3042 seen = seen || []; 3043 if(typeof(value)=="object") 3044 { 3045 if(seen.indexOf(value)>=0) return undefined; 3046 seen.push(value); 3047 3048 if(Array.isArray(value)) 3049 { 3050 var arr=[]; 3051 for(var i=0;i<value.length;i++) 3052 { 3053 var item=deviceJSONClone(value[i],seen); 3054 if(item!==undefined) arr.push(item); 3055 } 3056 seen.pop(); 3057 return arr; 3058 } 3059 3060 var out={}; 3061 for(var key in value) 3062 { 3063 var cloned=deviceJSONClone(value[key],seen); 3064 if(cloned!==undefined) out[key]=cloned; 3065 } 3066 seen.pop(); 3067 return out; 3068 } 3069 return undefined; 3070 } 3071 3072 this.deviceMetadata = function(owner,deviceRef) 3073 { 3074 if(!owner) return null; 3075 var entry=null; 3076 var device=null; 3077 var refHash=typeof(deviceRef)==="number" ? deviceRef : NaN; 3078 var refCode=Number.isInteger(refHash) ? "" : String(deviceRef || ""); 3079 3080 for(var key in this.attachments) 3081 { 3082 var candidate=this.attachments[key]; 3083 var candidateDevice=candidate && candidate.device; 3084 if(!candidate || candidate.owner!==owner || !candidateDevice) continue; 3085 3086 if(Number.isInteger(refHash)) 3087 { 3088 if(Number(candidateDevice.attach?.hash)!==refHash) continue; 3089 } 3090 else if(refCode && candidateDevice.id?.DCODE!==refCode) continue; 3091 3092 entry=candidate; 3093 device=candidateDevice; 3094 break; 3095 } 3096 if(!device) return null; 3097 3098 var id=device.id || {}; 3099 var state; 3100 try 3101 { 3102 state=typeof(device.getState)=="function" 3103 ? device.getState() 3104 : device.state; 3105 } 3106 catch(e) { state=undefined; } 3107 3108 return { 3109 "DCODE":String(id.DCODE || refCode || "") 3110 ,"instanceID":Number(device.attach?.hash) 3111 ,"hostPCODE":String(id.hostPCODE || owner.id?.PCODE || "") 3112 ,"coID":String(id.coID || entry.info?.coID || "") 3113 ,"description":String(id.description || entry.info?.description || "") 3114 ,"id":deviceJSONClone(id) || {} 3115 ,"config":deviceJSONClone(entry.info || {}) || {} 3116 ,"attachment":deviceJSONClone(device.attach || {}) || {} 3117 ,"ports":deviceJSONClone(device.ports || {}) || {} 3118 ,"state":deviceJSONClone(state) 3119 }; 3120 }; 3121 3122 function devicePopupElement() 3123 { 3124 var popup=document.getElementById("deviceConfig_popup"); 3125 if(popup) return popup; 3126 3127 popup=document.createElement("div"); 3128 popup.id="deviceConfig_popup"; 3129 popup.className="appbox"; 3130 popup.hidden=true; 3131 popup.style.cssText="position:fixed;z-index:6;width:360px;max-width:42vw;padding:8px;text-align:left"; 3132 (document.getElementById("feature_box") || document.body).appendChild(popup); 3133 return popup; 3134 } 3135 3136 function positionDevicePopup(popup) 3137 { 3138 var host=document.getElementById("slotConfig_popup"); 3139 if(!popup || !host || typeof(host.getBoundingClientRect)!="function") return; 3140 var rect=host.getBoundingClientRect(); 3141 var gap=8; 3142 var width=360; 3143 var left=rect.right+gap; 3144 if(typeof(window)!="undefined" && left+width>window.innerWidth-gap) 3145 left=Math.max(gap,rect.left-width-gap); 3146 popup.style.left=Math.round(left)+"px"; 3147 popup.style.top=Math.max(gap,Math.round(rect.top))+"px"; 3148 } 3149 3150 function devicePickerAnchorID(slotN) 3151 { 3152 return "device_add_"+Number(slotN); 3153 } 3154 3155 function devicePickerAnchor(slotN,active) 3156 {
3157 var anchor=document.getElementById(devicePickerAnchorID(slotN)); 3158 if(!anchor) return; 3159 anchor.style.background=active ? "rgba(255,255,0,.5)" : ""; 3160 anchor.setAttribute("aria-expanded",active ? "true" : "false"); 3161 } 3162 3163 function clearDevicePickerAnchors() 3164 { 3165 var anchors=document.querySelectorAll("[id^='device_add_']"); 3166 for(var i=0;i<anchors.length;i++) 3167 { 3168 anchors[i].style.background=""; 3169 anchors[i].setAttribute("aria-expanded","false"); 3170 } 3171 } 3172 3173 this.deviceConfig_close = function() 3174 { 3175 var popup=document.getElementById("deviceConfig_popup"); 3176 clearDevicePickerAnchors(); 3177 if(!popup) return false; 3178 popup.hidden=true; 3179 popup.innerHTML=""; 3180 popup.removeAttribute("data-mode"); 3181 popup.removeAttribute("data-slot"); 3182 return true; 3183 }; 3184 3185 this.devicePicker_message = function(text) 3186 { 3187 var el=document.getElementById("devicePicker_message"); 3188 if(!el) return false; 3189 el.textContent=String(text || ""); 3190 el.hidden=!text; 3191 return true; 3192 }; 3193 3194 this.devicePicker_entries = function(owner) 3195 { 3196 var out=[]; 3197 var cfg=owner && Array.isArray(owner.deviceConfig) ? owner.deviceConfig : []; 3198 var pcode=owner && owner.id ? owner.id.PCODE : ""; 3199 for(var i=0;i<cfg.length;i++) 3200 { 3201 var info=cfg[i]; 3202 if(!info || (info.hostPCODE && info.hostPCODE!==pcode)) continue; 3203 out.push(info); 3204 } 3205 return out; 3206 }; 3207 3208 this.devicePicker_popup = function(slotN) 3209 { 3210 slotN=Number(slotN); 3211 var owner=this.SLOT2obj(slotN); 3212 if(!owner) return false; 3213 3214 var popup=devicePopupElement(); 3215 if(!popup.hidden && popup.getAttribute("data-mode")==="picker" && 3216 Number(popup.getAttribute("data-slot"))===slotN) 3217 { 3218 this.deviceConfig_close(); 3219 return true; 3220 } 3221 3222 this.deviceConfig_close(); 3223 var entries=this.devicePicker_entries(owner); 3224 var pcode=peripheralPCODE(owner) || owner.id?.PCODE || "PERIPHERAL"; 3225 var description=peripheralDescription(owner); 3226 var slotTitle=slotN2name(slotN); 3227 var html="<button class='appbut' type='button' style='float:right' title='Close' aria-label='Close device picker' onclick=\"apple2plus.hwObj().io.deviceConfig_close()\">x</button>"; 3228 html += "<div style='padding-right:28px'><b>ADD DEVICE TO "+oCOM.escapeHTML(pcode)+"</b> — "+oCOM.escapeHTML(slotTitle)+"</div>"; 3229 if(description) 3230 html += "<div style='margin-top:2px;opacity:.75'>"+oCOM.escapeHTML(description)+"</div>"; 3231 html += "<div id='devicePicker_message' role='status' hidden style='margin-top:8px;padding:6px'></div>"; 3232 html += "<div style='margin-top:10px;display:flex;flex-direction:column;gap:4px'>"; 3233 3234 if(!entries.length) 3235 html += "<div class='appbox' style='float:none;padding:8px'>No compatible devices declared.</div>"; 3236 else 3237 { 3238 for(var i=0;i<entries.length;i++) 3239 { 3240 var info=entries[i]; 3241 var dcode=String(info.DCODE || info.coID || "device"); 3242 var devices=Array.isArray(owner.devices) ? owner.devices : []; 3243 var attachedCount=0; 3244 for(var d=0;d<devices.length;d++) 3245 if(devices[d]?.id?.DCODE===dcode) attachedCount++; 3246 3247 var ctor=globalThis[info.coID]; 3248 var available=typeof(ctor)==="function"; 3249 var state=available 3250 ? (attachedCount ? "Add another · Attached: "+attachedCount : "Available") 3251 : "Unavailable"; 3252 var title=info.description || dcode; 3253 if(attachedCount) title += " â "+attachedCount+" attached"; 3254 if(typeof(ctor)!=="function") title += " â device constructor unavailable"; 3255 3256 html += "<button class='appbut label device-picker-entry"+(available ? "" : " greyed")+"' type='button'" 3257 + " style='width:100%;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 8px;text-align:left;cursor:"+(available ? "pointer" : "default")+";'" 3258 + " title='"+oCOM.escapeHTML(title)+"'" 3259 + (available 3260 ? " onclick=\"event.stopPropagation();apple2plus.hwObj().io.devicePicker_select("+slotN+",'"+dcode+"')\"" 3261 : " disabled aria-disabled='true'") 3262 + ">" 3263 + "<span style='min-width:0'>" 3264 + "<span style='white-space:nowrap'><i class='"+slotDeviceIconClass(info.icon)+"'></i> <b>"+oCOM.escapeHTML(dcode)+"</b></span>" 3265 + (info.description ? "<br><span style='font-size:90%;opacity:.78'>"+oCOM.escapeHTML(info.description)+"</span>" : "") 3266 + "</span>" 3267 + "<span style='white-space:nowrap'>" 3268 + (available ? "<i class='fa fa-plus'></i> " : "") 3269 + oCOM.escapeHTML(state) 3270 + "</span>" 3271 + "</button>"; 3272 } 3273 } 3274 html += "</div>"; 3275 3276 popup.innerHTML=html; 3277 popup.setAttribute("data-mode","picker"); 3278 popup.setAttribute("data-slot",String(slotN)); 3279 popup.hidden=false; 3280 devicePickerAnchor(slotN,true); 3281 positionDevicePopup(popup); 3282 return true; 3283 }; 3284 3285 this.devicePicker_select = function(slotN,DCODE) 3286 { 3287 slotN=Number(slotN); 3288 DCODE=String(DCODE || ""); 3289 var owner=this.SLOT2obj(slotN); 3290 if(!owner || !DCODE) 3291 { 3292 this.devicePicker_message("Could not attach device: invalid target."); 3293 return false;
3294 } 3295 3296 var entries=this.devicePicker_entries(owner); 3297 var info=null; 3298 for(var i=0;i<entries.length;i++) 3299 if(String(entries[i].DCODE || entries[i].coID || "")===DCODE) { info=entries[i]; break; } 3300 if(!info) 3301 { 3302 this.devicePicker_message("Could not attach device: it is not compatible with this peripheral."); 3303 return false; 3304 } 3305 3306 var device=null; 3307 try 3308 { 3309 device=this.attach(owner,info,{"newInstance":true}); 3310 } 3311 catch(e) 3312 { 3313 console.error("Device attach failed",e); 3314 } 3315 3316 if(!device) 3317 { 3318 this.devicePicker_message("Could not attach device "+DCODE+"."); 3319 return false; 3320 } 3321 3322 this.deviceConfig_close(); 3323 this.slotConfig_refresh(slotN); 3324 this.refreshDeviceToolboxes({"id":"devices","default_slot":this.slot2ID(slotN)}); 3325 return true; 3326 }; 3327 3328 this.deviceConfig_detail = function(slotN,instanceHash) 3329 { 3330 slotN=Number(slotN); 3331 instanceHash=Number(instanceHash); 3332 var owner=this.SLOT2obj(slotN); 3333 var metadata=this.deviceMetadata(owner,instanceHash); 3334 if(!owner || !metadata) return false; 3335 3336 var liveDevice=null; 3337 var devices=Array.isArray(owner.devices) ? owner.devices : []; 3338 for(var i=0;i<devices.length;i++) 3339 { 3340 if(Number(devices[i]?.attach?.hash)===instanceHash) 3341 { 3342 liveDevice=devices[i]; 3343 break; 3344 } 3345 } 3346 var unit=liveDevice && typeof(liveDevice.getUnit)==="function" 3347 ? Number(liveDevice.getUnit()) 3348 : Number(liveDevice?.id?.deviceN); 3349 var bus=typeof(owner.getBus)==="function" ? owner.getBus() : null; 3350 var canMove=!!(bus && typeof(bus.move)==="function" && Number.isInteger(unit)); 3351 var upDisabled=!canMove || unit<=1; 3352 var downDisabled=!canMove || unit>=8; 3353 3354 var popup=devicePopupElement(); 3355 var description=metadata.description || ""; 3356 var instanceLabel="#"+oCOM.getHexWord(metadata.instanceID); 3357 var html="<button class='appbut' type='button' style='float:right' title='Close' onclick=\"apple2plus.hwObj().io.deviceConfig_close()\">x</button>" 3358 + "<div style='padding-right:28px'><b>"+oCOM.escapeHTML(metadata.DCODE)+" "+instanceLabel+"</b>" 3359 + (description ? "<br>"+oCOM.escapeHTML(description) : "") 3360 + "</div><div style='margin-top:10px'>" 3361 + "<button class='appbut' type='button' title='Move device one unit up'"+(upDisabled ? " disabled" : " onclick=\"event.stopPropagation();apple2plus.hwObj().io.deviceConfig_move("+slotN+","+metadata.instanceID+",-1)\"")+"><i class='fa fa-arrow-up'></i></button> " 3362 + "<button class='appbut' type='button' title='Move device one unit down'"+(downDisabled ? " disabled" : " onclick=\"event.stopPropagation();apple2plus.hwObj().io.deviceConfig_move("+slotN+","+metadata.instanceID+",1)\"")+"><i class='fa fa-arrow-down'></i></button> " 3363 + "<button class='appbut' type='button' title='Detach device' onclick=\"event.stopPropagation();apple2plus.hwObj().io.deviceConfig_eject("+slotN+","+metadata.instanceID+")\"><i class='fa fa-eject'></i></button>" 3364 + "</div>"; 3365 3366 popup.innerHTML=html; 3367 popup.hidden=false; 3368 positionDevicePopup(popup); 3369 return true; 3370 }; 3371 3372 this.deviceConfig_move = function(slotN,instanceHash,delta) 3373 { 3374 slotN=Number(slotN); 3375 instanceHash=Number(instanceHash); 3376 delta=Number(delta); 3377 if(delta!==-1 && delta!==1) return false; 3378 3379 var owner=this.SLOT2obj(slotN); 3380 if(!owner || !Number.isInteger(instanceHash)) return false; 3381 3382 var devices=Array.isArray(owner.devices) ? owner.devices : []; 3383 var device=null; 3384 for(var i=0;i<devices.length;i++) 3385 { 3386 if(Number(devices[i]?.attach?.hash)===instanceHash) 3387 { 3388 device=devices[i]; 3389 break; 3390 } 3391 } 3392 if(!device) return false;
3393 3394 var currentUnit=typeof(device.getUnit)==="function" 3395 ? Number(device.getUnit()) 3396 : Number(device.id?.deviceN); 3397 var targetUnit=currentUnit+delta; 3398 if(!Number.isInteger(currentUnit) || targetUnit<1 || targetUnit>8) return false; 3399 3400 var bus=typeof(owner.getBus)==="function" ? owner.getBus() : null; 3401 if(!bus || typeof(bus.move)!=="function") return false; 3402 3403 try 3404 { 3405 if(bus.move(device,targetUnit)!==device) return false; 3406 } 3407 catch(e) 3408 { 3409 console.error("Device unit move failed",e); 3410 return false; 3411 } 3412 3413 if(typeof(owner.onDeviceTopologyChanged)==="function") 3414 { 3415 try 3416 { 3417 owner.onDeviceTopologyChanged({ 3418 "type":"move" 3419 ,"DCODE":device.id?.DCODE || "" 3420 ,"device":device 3421 ,"instanceID":instanceHash 3422 ,"fromUnit":currentUnit 3423 ,"toUnit":targetUnit 3424 }); 3425 } 3426 catch(e) { console.error("Device topology move notification failed",e); } 3427 } 3428 3429 this.slotConfig_refresh(slotN); 3430 this.refreshDeviceToolboxes({"id":"devices","default_slot":this.slot2ID(slotN)}); 3431 this.deviceConfig_detail(slotN,instanceHash); 3432 return true; 3433 }; 3434 3435 this.deviceConfig_download = function(slotN,instanceHash) 3436 { 3437 slotN=Number(slotN); 3438 instanceHash=Number(instanceHash); 3439 var owner=this.SLOT2obj(slotN); 3440 var metadata=this.deviceMetadata(owner,instanceHash); 3441 if(!metadata) return false; 3442 try 3443 { 3444 var name=(metadata.DCODE+"_"+oCOM.getHexWord(metadata.instanceID)+"_"+slotN2name(slotN).replace("#","")).replace(/[^A-Za-z0-9_.-]/g,"_")+".json"; 3445 var json=JSON.stringify(metadata,null,2); 3446 oCOM.Download(name,new TextEncoder("utf-8").encode(json)); 3447 return true; 3448 } 3449 catch(e) 3450 { 3451 console.error("Device JSON download failed",e); 3452 return false; 3453 } 3454 }; 3455 3456 this.deviceConfig_downloadDevices = function(slotN) 3457 { 3458 slotN=Number(slotN); 3459 var owner=this.SLOT2obj(slotN); 3460 if(!owner) return false; 3461 3462 var devices=Array.isArray(owner.devices) ? owner.devices : []; 3463 var metadata=[]; 3464 for(var i=0;i<devices.length;i++) 3465 { 3466 var hash=Number(devices[i]?.attach?.hash);
3467 if(!Number.isInteger(hash)) continue; 3468 var item=this.deviceMetadata(owner,hash); 3469 if(item) metadata.push(item); 3470 } 3471 3472 try 3473 { 3474 var pcode=peripheralPCODE(owner) || "peripheral"; 3475 var slotName=slotN2name(slotN).replace("#",""); 3476 var name=(pcode+"_devices_"+slotName).replace(/[^A-Za-z0-9_.-]/g,"_")+".json"; 3477 var payload={ 3478 "hostPCODE":pcode 3479 ,"slot":slotN2name(slotN) 3480 ,"devices":metadata 3481 }; 3482 var json=JSON.stringify(payload,null,2); 3483 oCOM.Download(name,new TextEncoder("utf-8").encode(json)); 3484 return true; 3485 } 3486 catch(e) 3487 { 3488 console.error("Device collection JSON download failed",e); 3489 return false; 3490 } 3491 }; 3492 3493 this.deviceConfig_eject = function(slotN,instanceHash) 3494 { 3495 slotN=Number(slotN); 3496 instanceHash=Number(instanceHash); 3497 var owner=this.SLOT2obj(slotN); 3498 if(!owner || !Number.isInteger(instanceHash)) return false; 3499 if(!this.detachInstance(owner,instanceHash)) return false; 3500 3501 this.deviceConfig_close(); 3502 this.slotConfig_refresh(slotN); 3503 this.refreshDeviceToolboxes({"id":"devices","default_slot":this.slot2ID(slotN)}); 3504 return true; 3505 }; 3506 3507 /* 3508 * Render the live child devices attached to one peripheral. 3509 * 3510 * Use peripheral.devices rather than deviceConfig: Apple2IO.attach() 3511 * populates peripheral.devices only after a child device was actually 3512 * instantiated and attached. 3513 */ 3514 function slotDeviceTable_html(peripheral) 3515 { 3516 var devices = peripheral && Array.isArray(peripheral.devices) 3517 ? peripheral.devices.slice() 3518 : []; 3519 devices.sort(function(a,b) 3520 { 3521 var au=typeof(a?.getUnit)==="function" ? Number(a.getUnit()) : Number(a?.id?.deviceN); 3522 var bu=typeof(b?.getUnit)==="function" ? Number(b.getUnit()) : Number(b?.id?.deviceN); 3523 if(!Number.isInteger(au)) au=99; 3524 if(!Number.isInteger(bu)) bu=99; 3525 return au-bu; 3526 }); 3527 var body = ""; 3528 3529 for(var i=0;i<devices.length;i++) 3530 { 3531 var device = devices[i]; 3532 if(!device) continue; 3533 3534 body += "" 3535 + "<tr>" 3536 + "<td style='padding:4px 6px;vertical-align:top'>" 3537 + slotDeviceLabel_html(peripheral,device) 3538 + "</td>" 3539 + "<td style='padding:4px 6px;vertical-align:top;white-space:nowrap'>" 3540 + deviceInstanceLabel_html(device) 3541 + "</td>" 3542 + "<td style='padding:4px 6px;vertical-align:top'>" 3543 + slotDevicePorts_html(device) 3544 + "</td>" 3545 + "</tr>"; 3546 } 3547 3548 if(!body) 3549 body = "<tr><td colspan='3' style='padding:8px'>No devices attached.</td></tr>"; 3550 3551 var slotN=peripheral && peripheral.mount ? Number(peripheral.mount.slotN) : -1; 3552 var declaredDevices=io.devicePicker_entries(peripheral); 3553 var canPick=declaredDevices.length>0; 3554 var pcode=peripheralPCODE(peripheral) || peripheral?.id?.PCODE || "PERIPHERAL"; 3555 var deviceHeader="<div style='margin-top:10px;margin-bottom:3px;display:flex;align-items:center;gap:4px'>" 3556 + "<span style='margin-right:4px'><b>"+oCOM.escapeHTML(pcode)+"</b> — devices</span>"; 3557 if(Number.isInteger(slotN) && slotN>=0) 3558 { 3559 deviceHeader += "<button class='appbut' type='button' title='Download device JSON' aria-label='Download device JSON'" 3560 + " onclick=\"event.stopPropagation();apple2plus.hwObj().io.deviceConfig_downloadDevices("+slotN+")\">" 3561 + "<i class='fa fa-cloud-download-alt'></i></button>" 3562 + "<button class='appbut' type='button' id='"+devicePickerAnchorID(slotN)+"'" 3563 + " aria-label='Attach device' aria-haspopup='dialog' aria-expanded='false'" 3564 + " title='"+(canPick ? "Attach device" : "No compatible devices declared")+"'" 3565 + (canPick 3566 ? " onclick=\"event.stopPropagation();apple2plus.hwObj().io.devicePicker_popup("+slotN+")\"" 3567 : " disabled") 3568 + "><i class='fa fa-plus'></i></button>"; 3569 } 3570 deviceHeader += "</div>"; 3571 3572 return deviceHeader 3573 + "<table style='width:100%;border-collapse:collapse;margin-top:0px;text-align:left'>" 3574 + "<thead><tr style='border-bottom:1px solid #888'>" 3575 + "<th style='padding:4px 6px'>Device</th>" 3576 + "<th style='padding:4px 6px'>Instance</th>" 3577 + "<th style='padding:4px 6px'>Ports</th>" 3578 + "</tr></thead>" 3579 + "<tbody>"+body+"</tbody>" 3580 + "</table>"; 3581 } 3582 3583 /* 3584 * Match the labels used by Peripheral controls: appbut + label, 3585 * pictogram, DCODE and description/range in the tooltip. 3586 */ 3587 function slotDeviceLabel_html(peripheral,device) 3588 { 3589 var id=device && device.id ? device.id : {}; 3590 var deviceCode=String( 3591 id.DCODE || 3592 id.coID || 3593 (device && device.constructor && device.constructor.name) || 3594 "device" 3595 ); 3596 var description=id.description ? String(id.description) : ""; 3597 var icon=slotDeviceIconClass(id.icon); 3598 var instanceHash=Number(device && device.attach ? device.attach.hash : NaN); 3599 3600 return "" 3601 + "<div class='appbut label'" 3602 + " data-dcode='"+oCOM.escapeHTML(deviceCode)+"'" 3603 + (Number.isInteger(instanceHash) ? " data-instance='"+instanceHash+"'" : "") 3604 + " style='display:inline-block;cursor:pointer;white-space:nowrap;'" 3605 + (peripheral && peripheral.mount && Number.isInteger(instanceHash) 3606 ? " onclick='event.stopPropagation();apple2plus.hwObj().io.deviceConfig_detail("+Number(peripheral.mount.slotN)+","+instanceHash+")'" 3607 : "") 3608 + (description 3609 ? " title=\""+oCOM.escapeHTML(description)+"\"" 3610 : "") 3611 + ">" 3612 + "<i class=\""+icon+"\" aria-hidden=\"true\"></i> " 3613 + oCOM.escapeHTML(deviceCode) 3614 + "</div>"; 3615 } 3616 3617 function deviceInstanceLabel_html(device) 3618 { 3619 var hash=Number(device && device.attach ? device.attach.hash : NaN);
3620 return Number.isInteger(hash) ? "#"+oCOM.getHexWord(hash) : "—"; 3621 } 3622 3623 /* 3624 * Font Awesome icon identifiers are CSS class lists such as 3625 * "fa fa-keyboard". Do not pass the complete class list through 3626 * oCOM.escapeHTML(): that helper intentionally converts spaces to 3627 * , which turns the list into one invalid CSS class token. 3628 * 3629 * Instead, accept only ordinary CSS class-name characters in each token 3630 * and join the validated tokens with real spaces. 3631 */ 3632 function slotDeviceIconClass(value) 3633 { 3634 var tokens = String(value || "fa fa-cube") 3635 .trim() 3636 .split(/\s+/) 3637 .filter(function(token) 3638 { 3639 return /^[A-Za-z0-9_-]+$/.test(token); 3640 }); 3641 3642 return tokens.length ? tokens.join(" ") : "fa fa-cube"; 3643 } 3644 3645 function slotDevicePorts_html(device) 3646 { 3647 var ports = device && device.ports; 3648 if(!ports || typeof(ports)!="object") 3649 return "—"; 3650 3651 var names = Object.keys(ports).filter(function(name) 3652 { 3653 var port = ports[name] || {}; 3654 return String(port.visibility || "public").toLowerCase()!="internal"; 3655 }); 3656 if(!names.length) 3657 return "—"; 3658 3659 var html = "<div style='display:flex;flex-wrap:wrap;gap:3px'>"; 3660 3661 for(var i=0;i<names.length;i++) 3662 { 3663 var name = names[i]; 3664 var port = ports[name] || {}; 3665 var label = slotDevicePortLabel(name,port); 3666 var description = port.description ? String(port.description) : ""; 3667 3668 html += "" 3669 + "<div class=\"appbut label\"" 3670 + " style=\"display:inline-block;cursor:default;white-space:nowrap;" 3671 + "background:rgba(192,192,192,.35);\"" 3672 + (description 3673 ? " title=\""+oCOM.escapeHTML(description)+"\"" 3674 : "") 3675 + ">" 3676 + oCOM.escapeHTML(label) 3677 + "</div>"; 3678 } 3679 3680 return html+"</div>"; 3681 } 3682 3683 function slotDevicePortLabel(name,port) 3684 { 3685 port = port || {}; 3686 3687 /* 3688 * Structured bus/device ports provide their own human-facing label and 3689 * runtime instance selector. Keep transport details such as protocol, 3690 * direction and MIME metadata available in JSON without crowding the 3691 * compact Device-table chip. 3692 */ 3693 if(port.label!==undefined || port.unit!==undefined) 3694 { 3695 var label = port.label===undefined || port.label===null || String(port.label)==="" 3696 ? String(name) 3697 : String(port.label); 3698 var structured = [label]; 3699 if(port.unit!==undefined && port.unit!==null && port.unit!=="") 3700 structured.push("Unit "+String(port.unit)); 3701 return structured.join(" · "); 3702 } 3703 3704 /* Legacy port metadata keeps its existing direction/MIME rendering. */ 3705 var parts = [String("<b>"+name+"</b>")]; 3706 3707 if(port.direction) 3708 parts.push(String(port.direction.toUpperCase())); 3709 3710 if(port.mime) 3711 { 3712 var mime = Array.isArray(port.mime) 3713 ? port.mime.join(", ") 3714 : String(port.mime); 3715 if(mime) parts.push(mime); 3716 } 3717 3718 return parts.join("·"); 3719 } 3720 3721 /* 3722 * Render the address spaces and callbacks of one mounted peripheral. 3723 * 3724 * mount.ranges contains offsets relative to the Apple II I/O base, 3725 * normally $C000. peripheral.action contains the actual RD and WR 3726 * callback functions; these functions cannot be displayed through 3727 * JSON.stringify(). 3728 */ 3729 /* 3730 * Kept temporarily as a legacy renderer for comparison/debugging. 3731 * 3732 * It must not use the slotConfigDetail_html name: a later function 3733 * declaration with that name overrides the compact renderer above. 3734 */ 3735 function slotConfigDetail_legacy_html(slot) 3736 { 3737 var p = slot && slot.peripheral; 3738 3739 if(!p) 3740 { 3741 return "" 3742 + "<div class=appbox " 3743 + "style='margin-top:8px;padding:8px'>" 3744 + "No peripheral mounted." 3745 + "</div>"; 3746 } 3747 3748 /* 3749 * This order also follows the CPU-visible Apple II address space: 3750 * 3751 * HostIO : $C000-$C07F 3752 * SlotIO : $C080-$C0FF 3753 * SlotROM : $C100-$C7FF 3754 * HostROM : $C800-$CFFF 3755 */ 3756 var rangeNames = [ 3757 "HostIO" 3758 ,"SlotIO" 3759 ,"SlotROM" 3760 ,"HostROM" 3761 ]; 3762 3763 var rows = ""; 3764 3765 for(var i=0;i<rangeNames.length;i++) 3766 { 3767 var rangeName = rangeNames[i]; 3768 var range = peripheralRange(p,rangeName); 3769 3770 if(!range) continue; 3771 3772 var action = 3773 p.action && p.action[rangeName] 3774 ? p.action[rangeName] 3775 : {}; 3776 3777 rows += "" 3778 + "<tr>" 3779 3780 + "<td style='padding:4px 8px'>" 3781 + "<b>"+escapeHTML(rangeName)+"</b>" 3782 + "</td>" 3783 3784 + "<td style='padding:4px 8px;font-family:monospace'>" 3785 + fmtRange(range.from,range.to,0) 3786 + "</td>" 3787 3788 + "<td style='padding:4px 8px;font-family:monospace'>" 3789 + fmtRange(range.from,range.to,ioBase()) 3790 + "</td>" 3791 3792 + "<td style='padding:4px 8px;font-family:monospace'>" 3793 + actionCallbackLabel(action.RD) 3794 + "</td>" 3795 3796 + "<td style='padding:4px 8px;font-family:monospace'>" 3797 + actionCallbackLabel(action.WR) 3798 + "</td>" 3799 3800 + "</tr>"; 3801 } 3802 3803 if(rows=="") 3804 { 3805 rows = "" 3806 + "<tr>" 3807 + "<td colspan=5 style='padding:8px'>" 3808 + "No address ranges are configured for this peripheral." 3809 + "</td>" 3810 + "</tr>"; 3811 } 3812 3813 var description = peripheralDescription(p); 3814 3815 return "" 3816 + "<div class=appbox "
3817 + "style='width:650px;max-width:80vw;" 3818 + "overflow:auto;margin-top:8px;padding:8px'>" 3819 3820 + "<div>" 3821 + "<b>"+escapeHTML(peripheralPCODE(p))+"</b>" 3822 + ( 3823 description 3824 ? " — "+escapeHTML(description) 3825 : "" 3826 ) 3827 + "</div>" 3828 3829 + "<table " 3830 + "style='width:100%;border-collapse:collapse;" 3831 + "margin-top:8px;text-align:left'>" 3832 3833 + "<thead>" 3834 + "<tr style='border-bottom:1px solid #888'>" 3835 + "<th style='padding:4px 8px'>Space</th>" 3836 + "<th style='padding:4px 8px'>I/O offset</th>" 3837 + "<th style='padding:4px 8px'>CPU address</th>" 3838 + "<th style='padding:4px 8px'>RD function</th>" 3839 + "<th style='padding:4px 8px'>WR function</th>" 3840 + "</tr>" 3841 + "</thead>" 3842 3843 + "<tbody>" 3844 + rows 3845 + "</tbody>" 3846 3847 + "</table>" 3848 + "</div>"; 3849 } 3850 3851 function actionCallbackLabel(action) 3852 { 3853 var fn = action && action.callback; 3854 3855 if(typeof fn !== "function") 3856 return "—"; 3857 3858 /* 3859 * Function.bind() normally produces names such as 3860 * "bound read" and "bound write". The "bound" prefix is 3861 * an implementation detail and is not useful in the UI. 3862 */ 3863 var name = callbackLabel(fn).replace(/^bound\s+/,""); 3864 3865 return oCOM.escapeHTML( 3866 name == "(anonymous)" 3867 ? name 3868 : name+"()" 3869 ); 3870 } 3871 3872 ///////////////////////////////////////////////////////////////// 3873 3874 3875 3876 this.slotPicker_popup = function(ctx, slotTitle) 3877 { 3878 var popupId = "slotConfig_popup"; 3879 var anchorId = "d_slot_" + ctx.hostId + "_" + slotTitle.replace(/[^a-zA-Z0-9_-]/g, "_"); 3880 document.getElementById(anchorId).style = "background:rgba(255, 255, 0, 0.5); border-radius:10px" 3881 3882 var close = "<button class='appbut' style='float:right' onclick=\"oCOM.POPUP.toggle('" + popupId + "');document.getElementById('"+anchorId+"').style=''\">x</button>"; 3883 var html = ""; 3884 3885 html += "<div style=\"float:left\">ADD PERIPHERAL</div>"; 3886 html += close; 3887 html += "<br><br>"; 3888 3889 var io = apple2plus.hwObj().io; 3890 3891 console.assert( 3892 Array.isArray(io.slots), 3893 "io.slots must be an array" 3894 ); 3895 3896 console.assert( 3897 io.slot_ctx.peripheral_slots.slots === io.slots, 3898 "slot renderer must reference the canonical slot array" 3899 ); 3900 3901 console.assert( 3902 io.slots.length === slot_count + 1, 3903 "unexpected number of internal slot records" 3904 ); 3905 3906 var slotN = slotName2n(slotTitle); 3907 var targetSlot = io.slots[slotN]; 3908 var slotFit = slotR.slotFit && Array.isArray(slotR.slotFit[slotN]) 3909 ? slotR.slotFit[slotN] 3910 : []; 3911 var names = io.scanPeripheralContainers(); 3912 var pcodes = Object.keys(names).sort(); 3913 3914 html += "<div style='display:flex;flex-wrap:wrap;gap:2px'>"; 3915 3916 for(var i=0;i<pcodes.length;i++) 3917 { 3918 var id = names[pcodes[i]]; 3919 var cinfo = typeof(_CFG_PSLOT)!="undefined" ? _CFG_PSLOT[id.PCODE] : null; 3920 var compatible = slotFit.indexOf(id.PCODE)>=0; 3921 var addable = !!( 3922 targetSlot && 3923 !targetSlot.lock && 3924 !targetSlot.peripheral && 3925 id.slotLock!==true && 3926 compatible && 3927 cinfo 3928 ); 3929 var title = id.description || id.PCODE; 3930 3931 if(id.slotLock===true) 3932 title += " â fixed peripheral"; 3933 else if(!compatible) 3934 title += " â not compatible with "+slotTitle; 3935 3936 html += "<div class='appbut label"+(addable ? "" : " greyed")+"'" 3937 +" style='cursor:default;white-space:nowrap;'" 3938 +" title='"+oCOM.escapeHTML(title)+"'>" 3939 +"<button class='appbut skinny' type='button'" 3940 +(addable 3941 ? " title='Add to "+slotTitle+"' onclick=\"event.stopPropagation();apple2plus.hwObj().io.slotPicker_select('" + ctx.hostId + "','" + slotTitle + "','" + id.PCODE + "')\"" 3942 : " disabled") 3943 +"><i class='fa fa-plus'></i></button> " 3944 +"<i class='" + (id.icon || "fa fa-cube") + "'></i> " 3945 +oCOM.escapeHTML(id.PCODE) 3946 +"</div>"; 3947 } 3948 html += "</div>"; 3949 3950 3951 3952 /* 3953 // TODO: LOOKUP WHICH PERIPHERAL IS IN CURRENTLY IN 'slotTitle' 3954 3955 html += "<button class=appbut>" 3956 +"<a href=\"https://github.com/RetroAppleJS/RetroAppleJS.github.io/blob/main/docs/CONFIG.md#peripherals-list\" target=_blank> " 3957 +"<i class='fa fa-info-circle'></i>" 3958 +"</a>" 3959 +"</button> "; 3960 */ 3961 3962 3963 document.getElementById(popupId).innerHTML = html; 3964 3965 if(oCOM.POPUP.get_state(popupId)==false) document.getElementById(anchorId).style = ""; 3966 oCOM.POPUP.toggle(popupId); 3967 }; 3968 3969 this.slotPicker_select = function(hostId, slotTitle, pcode) 3970 { 3971 var ctx = this.slot_ctx[hostId]; 3972 var slots = ctx && Array.isArray(ctx.slots) ? ctx.slots : this.slots; 3973 var slot = slots.find(function(s) { return s.slotTitle === slotTitle; }); 3974 if (!slot || slot.lock || slot.peripheral) return false;
3975 3976 var io = oEMU.component.IO.self; 3977 var discovered = io ? io.scanPeripheralContainers() : {}; 3978 var id = discovered[pcode] || null; 3979 3980 var cinfo = typeof(_CFG_PSLOT) !== "undefined" && _CFG_PSLOT[pcode] ? _CFG_PSLOT[pcode] : null; 3981 var slotN = slotName2n(slotTitle); 3982 var slotFit = slotR.slotFit && Array.isArray(slotR.slotFit[slotN]) 3983 ? slotR.slotFit[slotN] 3984 : []; 3985 if(!id || !cinfo || id.slotLock===true || slotFit.indexOf(pcode)<0) 3986 return false; 3987 3988 var o = io.mount(cinfo,id,slotN,slotFit); 3989 if(!o || !o.pObj) return false; 3990 3991 for(var so in o.sInfo) slot[so] = o.sInfo[so]; 3992 slot.peripheral = o.pObj; 3993 3994 var model = typeof(EMU_system_get)=="function" ? EMU_system_get() : "A2P"; 3995 io.provisionPeripheral(o.pObj,model); 3996 refillEmptyIOActions(); 3997 3998 if(typeof(o.pObj.restart)=="function") 3999 o.pObj.restart(); 4000 4001 oCOM.POPUP.toggle("slotConfig_popup"); 4002 this.slotsRender(hostId); 4003 4004 if(typeof(this.refreshDeviceToolboxes)=="function") 4005 this.refreshDeviceToolboxes({"id":"devices"}); 4006 4007 return true; 4008 }; 4009 4010 this.slotPicker_info = function() { 4011 oCOM.POPUP.wipe(); 4012 oCOM.POPUP.html( 4013 "<b>Peripheral configuration</b><br><br>" + 4014 "To make a peripheral appear in this picker, add or modify its " + 4015 "entry in <code>CONFIG.md</code>.<br><br>" + 4016 "After editing <code>CONFIG.md</code>, compile it into " + 4017 "<code>COM_CONFIG.js</code> with:<br><br>" + 4018 "<code>tools/ConfigFile_updater.html</code><br><br>" + 4019 "Do not edit <code>COM_CONFIG.js</code> manually." 4020 ); 4021 }; 4022 4023// END 4024// ^ ^ ^ ^ 4025// | | | | 4026// ______ __ _ ___ _ 4027// .' ____ \ [ | / |_ .' ..](_) 4028// | (___ \_| | | .--. `| |-' .---. .--. _ .--. _| |_ __ .--./) 4029// _.____`. | |/ .'`\ \| | / /'`\]/ .'`\ \[ `.-. |'-| |-'[ | / /'`\; 4030// | \____) | | || \__. || |, | \__. | \__. | | | | | | | | | \ \._// 4031// \______.'[___]'.__.' \__/ '.___.' '.__.' [___||__][___] [___].',__` 4032// ( ( __)) 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 this.deviceBtn = function(arg) 4045 { 4046 var btn = document.getElementById(arg.id); 4047 if(btn == null) return; 4048 4049 // First reconcile the UI with the currently mounted topology. 4050 var slot = this.refreshDeviceToolboxes(arg); 4051 var slots = this.deviceSlots(); 4052 4053 if(slots.length == 0) return; 4054 if(arg.init === true) return; 4055 4056 if(slot == null || slots.indexOf(slot) < 0) slot = slots[0]; 4057 else slot = slots[(slots.indexOf(slot)+1)%slots.length]; 4058 4059 this.refreshDeviceToolboxes({"id":arg.id,"default_slot":slot}); 4060 } 4061 4062 this.deviceSlots = function() 4063 { 4064 var slots = []; 4065 for(var slotN=0;slotN<this.slots.length;slotN++) 4066 { 4067 if(this.SLOT2obj(slotN)) 4068 slots.push(this.slot2ID(slotN)); 4069 } 4070 return slots; 4071 } 4072 4073 this.deviceLabel = function(slot) 4074 { 4075 return slot==="H" ? "Hâ¹" : slot + "â¹"; 4076 } 4077 4078 this.deviceTopologySig = function() 4079 { 4080 var slots = this.deviceSlots(); 4081 var sig = []; 4082 4083 for(var i=0;i<slots.length;i++) 4084 { 4085 var s = slots[i]; 4086 var slotN = slotID2n(s); 4087 var peripheral = this.SLOT2obj(slotN); 4088 var pcode = peripheralPCODE(peripheral); 4089 var instance = peripheral && peripheral.mount ? peripheral.mount.hash : ""; 4090 var deviceCodes = []; 4091 var devices = peripheral && Array.isArray(peripheral.devices) 4092 ? peripheral.devices 4093 : []; 4094 4095 for(var d=0;d<devices.length;d++) 4096 { 4097 var device=devices[d]; 4098 var id=device && device.id; 4099 if(id && id.DCODE) 4100 { 4101 var unit=typeof(device.getUnit)==="function" ? Number(device.getUnit()) : Number(id.deviceN); 4102 var hash=Number(device.attach?.hash); 4103 deviceCodes.push(String(id.DCODE)+"#"+(Number.isInteger(hash
4103)?hash:"")+"@"+(Number.isInteger(unit)?unit:"")); 4104 } 4105 } 4106 4107 sig.push(String(s)+":"+pcode+":"+instance+":"+deviceCodes.join(",")); 4108 } 4109 4110 return sig.join("|"); 4111 } 4112 4113 this.refreshDeviceToolboxes = function(arg) 4114 { 4115 arg = arg || {}; 4116 var io = this; 4117 4118 function syncPeripheralLayoutContext(selectedSlot) 4119 { 4120 var layout = typeof(oLAYOUT) != "undefined" && oLAYOUT 4121 ? oLAYOUT 4122 : (typeof(window) != "undefined" ? window.oLAYOUT : null); 4123 if(!layout || typeof(layout.setPeripheralContext) != "function") return false; 4124 4125 if(selectedSlot === "H" || selectedSlot === null || selectedSlot === undefined) 4126 return layout.setPeripheralContext(null,""); 4127 4128 var slotN = Number(slotID2n(selectedSlot)); 4129 var peripheral = Number.isInteger(slotN) ? io.SLOT2obj(slotN) : null; 4130 var pcode = peripheralPCODE(peripheral); 4131 4132 // Navigation is authoritative: rebuild the selected scene from the 4133 // live peripheral and its attached devices before presenting it. 4134 if(peripheral && typeof(io.syncDeviceLayout) == "function") 4135 io.syncDeviceLayout(peripheral); 4136 if(peripheral && typeof(peripheral.syncLayoutVisuals) == "function") 4137 peripheral.syncLayoutVisuals(); 4138 4139 return layout.setPeripheralContext(slotN,pcode); 4140 } 4141 4142 var box = document.getElementById("device_toolbox_body"); 4143 var btn = document.getElementById(arg.id || "devices"); 4144 if(box == null || btn == null) return null; 4145 4146 var slots = this.deviceSlots(); 4147 var sig = this.deviceTopologySig(); 4148 var oldSig = box.getAttribute("data-topology"); 4149 4150 // Rebuild only when mounted devices changed. 4151 if(oldSig !== sig) 4152 { 4153 // display all Peripheral controls 4154 var slots = this.deviceSlots(); 4155 for(var slotN=0,html_arr=[];slotN<slots.length;slotN++) 4156 html_arr.push(this.deviceToolSlotHTML( slots[slotN] )); 4157 4158 box.innerHTML = html_arr.join(""); 4159 box.setAttribute("data-topology",sig); 4160 4161 if(typeof(EMU_mem_map)=="function" && typeof(apple2plus)=="object" && apple2plus!=null) 4162 EMU_mem_map(); 4163 } 4164 4165 if(slots.length == 0) 4166 { 4167 btn.innerHTML = "â "; 4168 btn.setAttribute("data-slot",""); 4169 this.showDeviceTool(null); 4170 syncPeripheralLayoutContext(null); 4171 return null; 4172 } 4173 4174 var cur = btn.getAttribute("data-slot"); 4175 var slot = cur==="H" ? "H" 4176 : (cur==null || cur==="" ? null : Number(cur)); 4177 4178 // default_slot explicitly requested slot if still mounted 4179 if(arg.default_slot === "H" && slots.indexOf("H") >= 0) slot = "H"; 4180 else if(typeof(arg.default_slot)=="number" && slots.indexOf(arg.default_slot) >= 0) slot = arg.default_slot; 4181 else if(slot == null || slots.indexOf(slot) < 0) slot = slots[0]; 4182 4183 btn.setAttribute("data-slot", slot==="H" ? "H" : String(slot)); 4184 btn.innerHTML = this.deviceLabel(slot); 4185 this.showDeviceTool(slot); 4186 syncPeripheralLayoutContext(slot); 4187 return slot; 4188 } 4189 4190 this.showDeviceTool = function(slot) 4191 { 4192 var all = document.querySelectorAll("[id^='device_tool_']"); 4193 for(var i=0;i<all.length;i++) 4194 all[i].hidden = true; 4195 4196 if(slot == null) return; 4197 4198 var el = document.getElementById("device_tool_" + (slot==="H" ? "H" : slot)); 4199 if(el) el.hidden = false; 4200 } 4201 4202 this.deviceToolSlotHTML = function(slotID) 4203 { 4204 var slotN = slotID2n(slotID); 4205 4206 var peripheral = this.SLOT2obj(slotN); 4207 var pcode = peripheralPCODE(peripheral); 4208 var model = typeof(EMU_system_get)=="function" ? EMU_system_get() : "A2P"; 4209 var ctx = 4210 { 4211 "io":this 4212 ,"peripheral":peripheral 4213 ,"slotN":slotN 4214 ,"slotID":slotID 4215 ,"model":model 4216 ,"toolboxID":"device_tool_"+slotID 4217 ,"devices":peripheral && Array.isArray(peripheral.devices) 4218 ? peripheral.devices 4219 : [] 4220 }; 4221 4222 if(peripheral && typeof(peripheral.deviceToolSlotHTML)=="function") 4223 { 4224 try 4225 { 4226 var html = peripheral.deviceToolSlotHTML(ctx);
4227 if(typeof(html)=="string" && html.length>0) return html; 4228 } 4229 catch(e) 4230 { 4231 console.error( 4232 "Peripheral toolbox failed for "+(pcode || slotID), 4233 e 4234 ); 4235 } 4236 } 4237 4238 var label = pcode || "EMPTY"; 4239 var description = peripheralDescription(peripheral); 4240 4241 4242 return "" 4243 + "<div class=toolbox id=\"device_tool_"+slotID+"\" hidden>" 4244 + " <div class=appbox style=\"text-align:left;height:63px;padding:0px 6px 0px 6px;\">" 4245 + " <b>#"+slotID+" "+oCOM.escapeHTML(label)+"</b><br>" 4246 + (description ? oCOM.escapeHTML(description)+"<br>" : "") 4247 + " No toolbox is available yet." 4248 + " </div>" 4249 + "</div>"; 4250 } 4251 4252 4253 function actionMapEntryCount(map) 4254 { 4255 var rows = []; 4256 var ops = orderedActionOps(map); 4257 4258 for(var oi=0;oi<ops.length;oi++) 4259 { 4260 var op = ops[oi]; 4261 var opMap = map[op]; 4262 var addrs = numericActionAddrs(opMap); 4263 var spans = buildActionMapSpans(op,opMap); 4264 var callbacks = []; 4265 4266 for(var ai=0;ai<addrs.length;ai++) 4267 if(callbacks.indexOf(opMap[addrs[ai]]) < 0) callbacks.push(opMap[addrs[ai]]); 4268 4269 rows.push({ 4270 "op": op 4271 ,"entries": addrs.length 4272 ,"spans": spans.length 4273 ,"callbacks": callbacks.length 4274 ,"relative": addrs.length ? fmtRange(addrs[0],addrs[addrs.length-1],0) : "" 4275 ,"absolute": addrs.length ? fmtRange(addrs[0],addrs[addrs.length-1],ioBase()) : "" 4276 }); 4277 } 4278 return rows; 4279 } 4280 4281 function actionMapSpanReport(map) 4282 { 4283 var rows = []; 4284 var ops = orderedActionOps(map); 4285 4286 for(var oi=0;oi<ops.length;oi++) 4287 { 4288 var spans = buildActionMapSpans(ops[oi],map[ops[oi]]); 4289 for(var si=0;si<spans.length;si++) 4290 { 4291 var span = spans[si]; 4292 var space = ioSpaceLabel(span.from,span.to); 4293 var meta = callbackMeta(span.callback); 4294 if(!meta.PCODE && space === "HostIO") 4295 meta = {"slotTitle":"board","PCODE":"BOARD","range":"HostIO"}; 4296 4297 rows.push({ 4298 "op": span.op 4299 ,"relative": fmtRange(span.from,span.to,0) 4300 ,"absolute": fmtRange(span.from,span.to,ioBase()) 4301 ,"bytes": span.to-span.from+1 4302 ,"space": space 4303 ,"slot": meta.slotTitle 4304 ,"PCODE": meta.PCODE 4305 ,"range": meta.range 4306 ,"callback": callbackLabel(span.callback) 4307 }); 4308 } 4309 } 4310 return rows; 4311 } 4312 4313 function slotConfigReport(cfg) 4314 { 4315 var rows = []; 4316 for(var i=0;i<cfg.length;i++) 4317 { 4318 var slot = cfg[i] || {}; 4319 var p = slot.peripheral || null; 4320 rows.push({ 4321 "slot": slot.slotTitle || "" 4322 ,"lock": slot.lock ? "yes" : "" 4323 ,"PCODE": peripheralPCODE(p) 4324 ,"container": peripheralCoID(p) 4325 ,"HostROM": reportRange(peripheralRange(p,"HostROM")) 4326 ,"SlotIO": reportRange(peripheralRange(p,"SlotIO")) 4327 ,"SlotROM": reportRange(peripheralRange(p,"SlotROM")) 4328 ,"description": peripheralDescription(p) 4329 }); 4330 } 4331 return rows; 4332 } 4333 4334 function buildActionMapSpans(op,opMap) 4335 { 4336 var addrs = numericActionAddrs(opMap); 4337 var rows = []; 4338 if(addrs.length==0) return rows; 4339 4340 var from = addrs[0]; 4341 var prev = addrs[0]; 4342 var callback = opMap[from]; 4343 4344 for(var i=1;i<addrs.length;i++) 4345 { 4346 var addr = addrs[i]; 4347 var nextCallback = opMap[addr]; 4348 4349 if(addr == prev+1 && nextCallback === callback) 4350 { 4351 prev = addr; 4352 continue; 4353 } 4354 4355 rows.push({"op":op,"from":from,"to":prev,"callback":callback}); 4356 from = prev = addr; 4357 callback = nextCallback; 4358 } 4359 rows.push({"op":op,"from":from,"to":prev,"callback":callback}); 4360 return rows; 4361 } 4362 4363 function numericActionAddrs(opMap) 4364 { 4365 if(!opMap || typeof opMap !== "object") return []; 4366 return Object.keys(opMap) 4367 .map(function(k){ return Number(k) }) 4368 .filter(function(n){ return Number.isFinite(n) }) 4369 .sort(function(a,b){ return a-b }); 4370 } 4371 4372 function orderedActionOps(map) 4373 { 4374 if(!map || typeof map !== "object") return []; 4375 var order = ["RD","WR","RR","BT","RG","SV","VA"]; 4376 return Object.keys(map) 4377 .filter(function(op){ return map[op] && typeof map[op] === "object" }) 4378 .sort(function(a,b) 4379 { 4380 var ai = order.indexOf(a); if(ai<0) ai = order.length; 4381 var bi = order.indexOf(b); if(bi<0) bi = order.length; 4382 return ai == bi ? a.localeCompare(b) : ai-bi; 4383 }); 4384 } 4385 4386 function callbackMeta(callback) 4387 { 4388 var m = callback && callback._ioReport; 4389 if(m) return m; 4390 4391 return { 4392 "slotTitle": "" 4393 ,"PCODE": "" 4394 ,"range": "" 4395 }; 4396 } 4397 4398 function callbackLabel(callback) 4399 { 4400 if(typeof callback !== "function") return String(callback); 4401 return callback.name || "(anonymous)"; 4402 } 4403 4404 function ioBase() 4405 { 4406 if(oEMU.system && oEMU.system.IORANGES && oEMU.system.IORANGES.HostIO) 4407 return oCOM.parseRngExpr(oEMU.system.IORANGES.HostIO).from; 4408 return 0xC000; 4409 } 4410 4411 function reportRange(range) 4412 { 4413 if(!range) return ""; 4414 return fmtRange(range.from,range.to,0)+" ("+fmtRange(range.from,range.to,ioBase())+")"; 4415 } 4416 4417 function fmtRange(from,to,base) 4418 { 4419 return "$"+(base+from).toString(16).toUpperCase()+"-$"+(base+to).toString(16).toUpperCase(); 4420 } 4421 4422 function ioSpaceLabel(from,to) 4423 { 4424 var hostIO = parseNamedRange("HostIO"); 4425 var hostROM = parseNamedRange("HostROM"); 4426 4427 if(within(from,to,hostIO)) return "HostIO"; 4428 if(within(from,to,hostROM)) return "HostROM"; 4429 4430 for(var n=0;n<8;n++) 4431 { 4432 var slotIO = parseNamedRange("SlotIO",n); 4433 var slotROM = parseNamedRange("SlotROM",n); 4434 if(within(from,to,slotIO)) return "SlotIO PR#"+n; 4435 if(within(from,to,slotROM)) return "SlotROM PR#"+n; 4436 } 4437 return ""; 4438 } 4439 4440 function parseNamedRange(name,n) 4441 { 4442 if(!oEMU.system || !oEMU.system.IORANGES || !oEMU.system.IORANGES[name]) return null; 4443 return oCOM.parseRngExpr(oEMU.system.IORANGES[name],{"n":n || 0,"base":ioBase()}); 4444 } 4445 4446 function within(from,to,range) 4447 { 4448 return range && from >= range.from && to <= range.to; 4449 } 4450 4451}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.