More VT100 video support (untested)

This commit is contained in:
Jeff Parsons 2016-07-29 18:03:00 -07:00 committed by Jeff Parsons
commit 21fc2e0639
9 changed files with 734 additions and 444 deletions

View file

@ -63,6 +63,13 @@ if (NODE) {
* interruptRate: normally the same as (or some multiple of) refreshRate (eg, 120)
* refreshRate: how many times updateScreen() should be performed per second (eg, 60)
*
* In addition, if a text-only display is being emulated, define the following properties:
*
* fontROM: URL of font ROM
* fontColor: default is white
* cellWidth: number (eg, 10 for VT100)
* cellHeight: number (eg, 10 for VT100)
*
* We record all the above values now, but we defer creation of the frame buffer until our initBus()
* handler is called. At that point, we will also compute the extent of the frame buffer, determine the
* appropriate "cell" size (ie, the number of pixels that updateScreen() will fetch and process at once),
@ -77,7 +84,7 @@ if (NODE) {
* bufferRotate is an alternative to screenRotate; you may set one or the other (but not both) to -90 to
* enable different approaches to counter-clockwise 90-degree image rotation. screenRotate uses canvas
* transformation methods (translate(), rotate(), and scale()), while bufferRotate inverts the dimensions
* of the off-screen buffer and then relies on setPixel() to "rotate" the data into it.
* of the off-screen buffer and then relies on setPixel() to "rotate" the data into the proper location.
*
* @constructor
* @extends Component
@ -104,10 +111,16 @@ function Video(parmsVideo, canvas, context, textarea, container)
var sFormat = parmsVideo['bufferFormat'];
this.nFormat = sFormat && Video.FORMATS[sFormat.toLowerCase()] || Video.FORMAT.UNKNOWN;
this.cxBuffer = parmsVideo['bufferCols'];
this.cyBuffer = parmsVideo['bufferRows'];
this.nColsBuffer = parmsVideo['bufferCols'];
this.nRowsBuffer = parmsVideo['bufferRows'];
this.cxCellDefault = this.cxCell = parmsVideo['cellWidth'] || 1;
this.cyCellDefault = this.cyCell = parmsVideo['cellHeight'] || 1;
this.abFontData = null;
this.nBitsPerPixel = parmsVideo['bufferBits'] || 1;
this.iBitFirstPixel = parmsVideo['bufferLeft'] || 0;
this.rotateBuffer = parmsVideo['bufferRotate'];
if (this.rotateBuffer) {
this.rotateBuffer = this.rotateBuffer % 360;
@ -126,6 +139,10 @@ function Video(parmsVideo, canvas, context, textarea, container)
this.textareaScreen = textarea;
this.inputScreen = textarea || canvas || null;
this.xScreenOffset = this.yScreenOffset = 0;
this.cxScreenCell = (this.cxScreen / this.nColsBuffer)|0;
this.cyScreenCell = (this.cyScreen / this.nRowsBuffer)|0;
/*
* Support for disabling (or, less commonly, enabling) image smoothing, which all browsers
* seem to support now (well, OK, I still have to test the latest MS Edge browser), despite
@ -155,6 +172,10 @@ function Video(parmsVideo, canvas, context, textarea, container)
if (this.rotateScreen) {
this.rotateScreen = this.rotateScreen % 360;
if (this.rotateScreen > 0) this.rotateScreen -= 360;
/*
* TODO: Consider also disallowing any rotateScreen value if bufferRotate was already set; setting
* both is most likely a mistake, but perhaps it's also a legitimate way of enabling 180-degree rotation?
*/
if (this.rotateScreen != -90) {
this.notice("unsupported screen rotation: " + this.rotateScreen);
this.rotateScreen = 0;
@ -165,23 +186,7 @@ function Video(parmsVideo, canvas, context, textarea, container)
}
}
this.initColors();
/*
* Allocate off-screen buffers.
*/
var cxBuffer = this.cxBuffer;
var cyBuffer = this.cyBuffer;
if (this.rotateBuffer) {
cxBuffer = this.cyBuffer;
cyBuffer = this.cxBuffer;
}
this.imageBuffer = this.contextScreen.createImageData(cxBuffer, cyBuffer);
this.canvasBuffer = document.createElement("canvas");
this.canvasBuffer.width = cxBuffer;
this.canvasBuffer.height = cyBuffer;
this.contextBuffer = this.canvasBuffer.getContext("2d");
this.initBuffers();
/*
* Here's the gross code to handle full-screen support across all supported browsers. The lack of standards
@ -248,6 +253,61 @@ Video.FORMATS = {
"vt100": Video.FORMAT.VT100
};
Video.VT100 = {
FONT: {
NORML: 0,
DWIDE: 1,
DHIGH: 2
},
LINETERM: 0x7F,
LINEATTR: {
MASK: 0xE0,
SCROLL: 0x80,
ADDRMASK: 0x1F,
FONTMASK: 0x60,
NORML: 0x60,
DWIDE: 0x40,
DHIGH_TOP: 0x20,
DHIGH_BOT: 0x00
}
};
/**
* initBuffers()
*/
Video.prototype.initBuffers = function()
{
/*
* Allocate off-screen buffers now
*/
this.cxBuffer = this.nColsBuffer * this.cxCell;
this.cyBuffer = this.nRowsBuffer * this.cyCell;
var cxBuffer = this.cxBuffer;
var cyBuffer = this.cyBuffer;
if (this.rotateBuffer) {
cxBuffer = this.cyBuffer;
cyBuffer = this.cxBuffer;
}
this.imageBuffer = this.contextScreen.createImageData(cxBuffer, cyBuffer);
this.canvasBuffer = document.createElement("canvas");
this.canvasBuffer.width = cxBuffer;
this.canvasBuffer.height = cyBuffer;
this.contextBuffer = this.canvasBuffer.getContext("2d");
this.initColors();
if (this.cxCell > 1) {
this.aFonts = {};
this.abLineBuffer = new Array(132);
this.aFonts[Video.VT100.FONT.NORML] = this.createFontVariation(this.cxCell, this.cyCell);
this.aFonts[Video.VT100.FONT.DWIDE] = this.createFontVariation(this.cxCell*2, this.cyCell);
this.aFonts[Video.VT100.FONT.DHIGH] = this.createFontVariation(this.cxCell*2, this.cyCell*2);
}
};
/**
* initBus(cmp, bus, cpu, dbg)
*
@ -336,74 +396,10 @@ Video.prototype.doneLoad = function(sURL, sFontData, nErrorCode)
}
/*
* Translate the character data into separate "fonts", each of which will be a separate canvas object, with all
* 256 characters arranged in a 16x16 grid.
* Minimal font data validation, just to make sure we're not getting garbage from the server...
*/
if (ab.length == 2048) {
/*
* The assumption here is that we're dealing with the original (IBM) MDA/CGA font data, which apparently
* was identical on both MDA and CGA cards (even though the former had no use for the latter, and vice versa).
*
* First, let's take a look at the MDA portion of the data. Here are the first few rows of MDA font data,
* at the 0K and 2K boundaries:
*
* 00000000 00 00 00 00 00 00 00 00 00 00 7e 81 a5 81 81 bd |..........~.....|
* 00000010 00 00 7e ff db ff ff c3 00 00 00 36 7f 7f 7f 7f |..~........6....|
* ...
* 00000800 00 00 00 00 00 00 00 00 99 81 7e 00 00 00 00 00 |..........~.....|
* 00000810 e7 ff 7e 00 00 00 00 00 3e 1c 08 00 00 00 00 00 |..~.....>.......|
*
* 8 bytes of data from a row in each of the 2K chunks are combined to form a 8-bit wide character with
* a maximum height of 16 bits. Assembling the bits for character 0x01 (a happy face), we observe the following:
*
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x0008
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x0009
* 0 1 1 1 1 1 1 0 <== 7e from offset 0x000A
* 1 0 0 0 0 0 0 1 <== 81 from offset 0x000B
* 1 0 1 0 0 1 0 1 <== a5 from offset 0x000C
* 1 0 0 0 0 0 0 1 <== 81 from offset 0x000D
* 1 0 0 0 0 0 0 1 <== 81 from offset 0x000E
* 1 0 1 1 1 1 0 1 <== bd from offset 0x000F
* 1 0 0 1 1 0 0 1 <== 99 from offset 0x0808
* 1 0 0 0 0 0 0 1 <== 81 from offset 0x0809
* 0 1 1 1 1 1 1 0 <== 7e from offset 0x080A
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x080B
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x080C
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x080D
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x080E
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x080F
*
* In the second 2K chunk, we observe that the last two bytes of every font cell definition are zero;
* this confirms our understanding that MDA font cell size is 8x14.
*
* Finally, there's the issue of screen cell size, which is actually 9x14 on the MDA. We compensate for that
* by building a 9x14 font, even though there's only 8x14 bits of data. As http://www.seasip.info/VintagePC/mda.html
* explains:
*
* "For characters C0h-DFh, the ninth pixel column is a duplicate of the eighth; for others, it's blank."
*
* This last point is confirmed by "The IBM Personal Computer From The Inside Out", p.295:
*
* "Another unique feature of the monochrome adapter is a set of line-drawing and area-fill characters
* that give continuous lines and filled areas. This is unusual for a display with a 9x14 character box
* because the character generator provides a row only eight dots wide. On most displays, a blank 9th
* dot is then inserted between characters. On the monochrome display, there is circuitry that duplicates
* the 8th dot into the 9th dot position for characters whose ASCII codes are 0xB0 [sic] through 0xDF."
*
* However, the above text is mistaken about the start of the range. While there ARE line-drawing characters
* in the range 0xB0-0xBF, none of them extend all the way to the right edge; IBM carefully segregated them.
* And in fact, characters 0xB0-0xB2 contain hash patterns that you would NOT want extended into the 9th column.
*
* The CGA font is part of the same ROM. In fact, there are TWO CGA fonts in the ROM: a thin 5x7 "single dot"
* font located at offset 0x1000, and a thick 7x7 "double dot" font at offset 0x1800. The latter is the default
* font, unless overridden by a jumper setting on the CGA card, so it is our default CGA font as well (although
* someday we may provide a virtual jumper setting that allows you to select the thinner font).
*
* The first offset we must pass to setFontData() is the offset of the CGA font; we choose the thicker "double dot"
* CGA font at 0x1800 (which was the PC's default font as well), instead of the thinner "single dot" font at 0x1000.
* The second offset is for the MDA font.
*/
// this.setFontData(ab);
this.abFontdata = ab;
}
else {
this.notice("Unrecognized font data length (" + ab.length + ")");
@ -424,6 +420,87 @@ Video.prototype.doneLoad = function(sURL, sFontData, nErrorCode)
if (this.contextScreen || this.dbg) this.setReady();
};
/**
* createFontVariation(cxCell, cyCell, rgbOn, fUnderline, fReverse)
*
* This creates a 16x16 character grid for the requested font variation. Variations include:
*
* 1) no variation (cell size is this.cxCell x this.cyCell)
* 2) double-wide characters (cell size is this.cxCell*2 x this.cyCell)
* 3) double-high characters (cell size is this.cxCell x this.cyCell*2)
* 4) double-both characters (cell size is this.cxCell*2 x this.cyCell*2)
* 5) all of the above with underlining enabled
* 6) all of the above with reverse video enabled
*
* @this {Video}
* @param {number} cxCell is the target width of each character in the grid
* @param {number} cyCell is the target height of each character in the grid
* @param {Array} [rgbOn] contains the RGB values for the font (default is white)
* @param {boolean} [fUnderline]
* @param {boolean} [fReverse]
* @return {Object}
*/
Video.prototype.createFontVariation = function(cxCell, cyCell, rgbOn, fUnderline, fReverse)
{
/*
* On a VT100, cxCell,cyCell is initially 10,10, but may change to 9,10 for 132-column mode.
* However, regardless of mode, cxCellDefault,cyCellDefault retain the initial values of 10,10.
*/
this.assert(cxCell == this.cxCell || cxCell == this.cxCell*2);
this.assert(cyCell == this.cyCell || cyCell == this.cyCell*2);
/*
* Create a font canvas that is both 16 times the target character width and the target character height,
* ensuring that it will accommodate 16x16 characters (for a maximum of 256). Note that the VT100 font ROM
* defines only 128 characters, so that canvas will contain only 16x8 entries.
*/
var nFontBytesPerChar = this.cxCellDefault <= 8? 8 : 16;
var nFontByteOffset = nFontBytesPerChar > 8? 15 : 0;
var nChars = this.abFontData.length / nFontBytesPerChar;
var font = {cxCell: cxCell, cyCell: cyCell};
font.canvas = document.createElement("canvas");
font.canvas.width = cxCell * 16;
font.canvas.height = cyCell * (nChars / 16);
font.context = font.canvas.getContext("2d");
var imageChar = font.context.createImageData(cxCell, cyCell);
for (var iChar = 0; iChar < nChars; iChar++) {
for (var y = 0, yDst = y; y < this.cyCell; y++) {
var offFontData = iChar * nFontBytesPerChar + ((nFontByteOffset + y) & (nFontBytesPerChar - 1));
var b = this.abFontData[offFontData];
for (var nRows = 0; nRows < (cyCell / this.cyCell); nRows++) {
for (var x = 0, xDst = x; x < this.cxCell; x++) {
var bit = b & (0x80 >> x);
for (var nCols = 0; nCols < (cxCell / this.cxCell); nCols++) {
this.setPixel(imageChar, xDst, yDst, bit? 1 : 0);
xDst++;
}
}
yDst++;
}
}
/*
* (iChar >> 4) performs the integer equivalent of Math.floor(iChar / 16), and (iChar & 0xf) is the equivalent of (iChar % 16).
*/
font.context.putImageData(imageChar, (iChar & 0xf) * cxCell, (iChar >> 4) * cyCell);
}
/*
* Enable this code if you want to see what the generated font looks like....
*
if (MAXDEBUG) {
var iSrcColor = (iColor == 15? 0 : iColor + 1);
this.contextScreen.fillStyle = aCSSColors[iSrcColor];
this.contextScreen.fillRect(iColor*(font.cxCell<<2), 0, font.canvas.width>>2, font.cyCell<<4);
this.contextScreen.drawImage(font.canvas, 0, iColor*(font.cyCell<<4), font.canvas.width>>2, font.cyCell<<4, iColor*(font.cxCell<<2), 0, font.canvas.width>>2, font.cyCell<<4);
}
*/
return font;
};
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
@ -637,6 +714,47 @@ Video.prototype.setPixel = function(imageBuffer, x, y, bPixel)
imageBuffer.data[index+3] = rgb[3];
};
/**
* updateChar(font, col, row, data, context)
*
* Updates a particular character cell (row,col) in the associated window.
*
* @this {Video}
* @param {Object} font
* @param {number} col
* @param {number} row
* @param {number} data
* @param {Object} [context]
*/
Video.prototype.updateChar = function(font, col, row, data, context)
{
/*
* The caller MUST promise this.nFont is defined, and that the font in this.aFonts[this.nFont] has been loaded.
*/
var xDst, yDst;
var bChar = data & 0x7f;
if (context) {
xDst = col * font.cxCell;
yDst = row * font.cyCell;
} else {
xDst = col * this.cxScreenCell + this.xScreenOffset;
yDst = row * this.cyScreenCell + this.yScreenOffset;
}
/*
* (bChar & 0xf) is the equivalent of (bChar % 16), and (bChar >> 4) is the equivalent of Math.floor(bChar / 16)
*/
var xSrc = (bChar & 0xf) * font.cxCell;
var ySrc = (bChar >> 4) * font.cyCell;
if (context) {
context.drawImage(font.canvas, xSrc, ySrc, font.cxCell, font.cyCell, xDst, yDst, font.cxCell, font.cyCell);
} else {
this.contextScreen.drawImage(font.canvas, xSrc, ySrc, font.cxCell, font.cyCell, xDst, yDst, this.cxScreenCell, this.cyScreenCell);
}
};
/**
* updateScreen(n)
*
@ -657,19 +775,22 @@ Video.prototype.updateScreen = function(n)
var fUpdate = true;
if (n >= 0) {
if (!(n & 1)) {
/*
* On even updates, call cpu.requestINTR(1), and also update our copy of the screen.
*/
this.cpu.requestINTR(1);
} else {
/*
* On odd updates, call cpu.requestINTR(2), but do NOT update our copy of the screen, because
* the machine has presumably only updated the top half of the frame buffer at this point; it will
* update the bottom half of the frame buffer after acknowledging this interrupt.
*/
this.cpu.requestINTR(2);
fUpdate = false;
if (this.interruptRate) {
if (!(n & 1)) {
/*
* On even updates, call cpu.requestINTR(1), and also update our copy of the screen.
*/
this.cpu.requestINTR(1);
} else {
/*
* On odd updates, call cpu.requestINTR(2), but do NOT update our copy of the screen, because
* the machine has presumably only updated the top half of the frame buffer at this point; it will
* update the bottom half of the frame buffer after acknowledging this interrupt.
*/
this.cpu.requestINTR(2);
fUpdate = false;
}
}
/*
@ -688,8 +809,93 @@ Video.prototype.updateScreen = function(n)
this.nCyclesPrev = nCycles;
this.printMessage("updateScreen(" + n + "): clean=" + fClean + ", update=" + fUpdate + ", cycles=" + nCycles + ", delta=" + nCyclesDelta);
}
if (!fUpdate) return;
if (this.cxCell > 1) {
this.updateScreenText();
} else {
this.updateScreenGraphics();
}
};
/**
* updateScreenText()
*
* @this {Video}
*/
Video.prototype.updateScreenText = function()
{
switch(this.nFormat) {
case Video.FORMAT.VT100:
this.updateScreenVT100();
break;
}
};
/**
* updateScreenVT100()
*
* @this {Video}
*/
Video.prototype.updateScreenVT100 = function()
{
var addrNext = this.addrBuffer, attrNext = -1;
for (var iRow = 0; iRow < this.nRowsBuffer+1; iRow++) {
/*
* Fill the line buffer
*/
var cbCols = 0;
var addr = addrNext;
var attr = attrNext;
while (cbCols < this.abLineBuffer.length) {
var data = this.bus.getByteDirect(addr++);
if ((data & Video.VT100.LINETERM) == Video.VT100.LINETERM) {
var b = this.bus.getByteDirect(addr++);
attrNext = b & Video.VT100.LINEATTR.FONTMASK;
addrNext = this.addrBuffer | ((b & Video.VT100.LINEATTR.ADDRMASK) << 8) | this.bus.getByteDirect(addr);
break;
}
this.abLineBuffer[cbCols++] = data;
}
/*
* Display the line buffer
*/
if (attr >= 0) {
var idFont, cyClip = 0;
switch(attr) {
case Video.VT100.LINEATTR.NORML:
idFont = Video.VT100.FONT.NORML;
break;
case Video.VT100.LINEATTR.DWIDE:
idFont = Video.VT100.FONT.DWIDE;
break;
case Video.VT100.LINEATTR.DHIGH_TOP:
idFont = Video.VT100.FONT.DHIGH;
break;
case Video.VT100.LINEATTR.DHIGH_BOT:
idFont = Video.VT100.FONT.DHIGH;
cyClip = this.cyCell;
break;
}
var font = this.aFonts[idFont];
if (font) {
for (var iCol = 0; iCol < cbCols; iCol++) {
this.updateChar(font, iCol, iRow-1, this.abLineBuffer[iCol], this.contextBuffer);
}
}
}
}
};
/**
* updateScreenGraphics()
*
* @this {Video}
*/
Video.prototype.updateScreenGraphics = function()
{
var addr = this.addrBuffer;
var addrLimit = addr + this.sizeBuffer;

View file

@ -1068,6 +1068,18 @@
<xsl:otherwise>false</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="cellWidth">
<xsl:choose>
<xsl:when test="@cellWidth"><xsl:value-of select="@cellWidth"/></xsl:when>
<xsl:otherwise>1</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="cellHeight">
<xsl:choose>
<xsl:when test="@cellHeight"><xsl:value-of select="@cellHeight"/></xsl:when>
<xsl:otherwise>1</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="charCols">
<xsl:choose>
<xsl:when test="@cols"><xsl:value-of select="@cols"/></xsl:when>
@ -1090,6 +1102,12 @@
<xsl:otherwise/>
</xsl:choose>
</xsl:variable>
<xsl:variable name="fontColor">
<xsl:choose>
<xsl:when test="@fontColor"><xsl:value-of select="@fontColor"/></xsl:when>
<xsl:otherwise/>
</xsl:choose>
</xsl:variable>
<xsl:variable name="touchScreen">
<xsl:choose>
<xsl:when test="@touchscreen"><xsl:value-of select="@touchscreen"/></xsl:when>
@ -1132,7 +1150,7 @@
<xsl:call-template name="component">
<xsl:with-param name="machine" select="$machine"/>
<xsl:with-param name="class">video</xsl:with-param>
<xsl:with-param name="parms">,model:'<xsl:value-of select="$model"/>',mode:<xsl:value-of select="$mode"/>,screenWidth:<xsl:value-of select="$screenWidth"/>,screenHeight:<xsl:value-of select="$screenHeight"/>,screenColor:'<xsl:value-of select="$screenColor"/>',screenRotate:<xsl:value-of select="$screenRotate"/>,bufferAddr:<xsl:value-of select="$bufferAddr"/>,bufferRAM:<xsl:value-of select="$bufferRAM"/>,bufferFormat:'<xsl:value-of select="$bufferFormat"/>',bufferCols:<xsl:value-of select="$bufferCols"/>,bufferRows:<xsl:value-of select="$bufferRows"/>,bufferBits:<xsl:value-of select="$bufferBits"/>,bufferLeft:<xsl:value-of select="$bufferLeft"/>,bufferRotate:<xsl:value-of select="$bufferRotate"/>,memory:<xsl:value-of select="$memory"/>,switches:'<xsl:value-of select="$switches"/>',scale:<xsl:value-of select="$scale"/>,charCols:<xsl:value-of select="$charCols"/>,charRows:<xsl:value-of select="$charRows"/>,fontROM:'<xsl:value-of select="$fontROM"/>',touchScreen:'<xsl:value-of select="$touchScreen"/>',autoLock:<xsl:value-of select="$autoLock"/>,aspectRatio:<xsl:value-of select="$aspectRatio"/>,smoothing:<xsl:value-of select="$smoothing"/>,interruptRate:<xsl:value-of select="$interruptRate"/>,refreshRate:<xsl:value-of select="$refreshRate"/></xsl:with-param>
<xsl:with-param name="parms">,model:'<xsl:value-of select="$model"/>',mode:<xsl:value-of select="$mode"/>,screenWidth:<xsl:value-of select="$screenWidth"/>,screenHeight:<xsl:value-of select="$screenHeight"/>,screenColor:'<xsl:value-of select="$screenColor"/>',screenRotate:<xsl:value-of select="$screenRotate"/>,bufferAddr:<xsl:value-of select="$bufferAddr"/>,bufferRAM:<xsl:value-of select="$bufferRAM"/>,bufferFormat:'<xsl:value-of select="$bufferFormat"/>',bufferCols:<xsl:value-of select="$bufferCols"/>,bufferRows:<xsl:value-of select="$bufferRows"/>,bufferBits:<xsl:value-of select="$bufferBits"/>,bufferLeft:<xsl:value-of select="$bufferLeft"/>,bufferRotate:<xsl:value-of select="$bufferRotate"/>,memory:<xsl:value-of select="$memory"/>,switches:'<xsl:value-of select="$switches"/>',scale:<xsl:value-of select="$scale"/>,cellWidth:<xsl:value-of select="$cellWidth"/>,cellHeight:<xsl:value-of select="$cellHeight"/>,charCols:<xsl:value-of select="$charCols"/>,charRows:<xsl:value-of select="$charRows"/>,fontROM:'<xsl:value-of select="$fontROM"/>',fontColor:'<xsl:value-of select="$fontColor"/>',touchScreen:'<xsl:value-of select="$touchScreen"/>',autoLock:<xsl:value-of select="$autoLock"/>,aspectRatio:<xsl:value-of select="$aspectRatio"/>,smoothing:<xsl:value-of select="$smoothing"/>,interruptRate:<xsl:value-of select="$interruptRate"/>,refreshRate:<xsl:value-of select="$refreshRate"/></xsl:with-param>
</xsl:call-template>
</xsl:template>