In part one of the Apple II weather display I quickly went over how data is fetched and phrased. Now its time to do something with it in part 2. In the order of functions I do the text parts first, and though its very similar to the process that the radar image goes through, its in monochrome and a bit simpler to explain. Before I go into how it works I should explain how I am dividing the Apple II’s screen.
The high resolution mode on an Apple II gives you 280×160 with 4 lines of text on the bottom, or 280×192 full screen. I will be using 280×192 full screen, because as useful as the text can be, it also would show the endless stream of gibberish while updating, and since the entire color video mode of the Apple II design is a NTSC hack, its not all that pretty on a standard TV while also displaying color graphics. I also divide the screen into “blocks” 35 pixels wide each, this came about due to my encoding system.
Without getting into high ASCII or special characters I decided to use a base 36 type numbering system, we only need 35 unique characters to represent any pixel on a line within a block. The value of 1 means the first pixel in a given block and so on to Z being pixel number 35 in a block, and up to 8 blocks for the entire screen. Block separators and other functions can be sent using lower case characters, making a nice n easy plain text system. For example, if the apple received bbb123Z it means skip to block 4 and place a pixel at 1,2,3, and 35 within that block (and if there is no pixels at all for that given line a n is sent).
Looking at text.lua …
-- Weather Underground to Apple // -- 2011 Kevin Dady -- -- Text to Graphics: -- take text from web.data -- make text images with image magick -- phrase *.xpm files to apple // -- send text images -- end text = {} text.data = {} text.data.input = {} text.data.packed = {{},{},{},{}} text.data.apl2 = {{},{},{}} text.createIMG = function() if web.data[1] == nil then web.data[1] = "No Advisories" end if web.data[3] == nil then web.data[3] = web.data[7] end -- create the bottom text cmd.imageMGK(" -background black -fill white -font req/VeraMoBd.ttf".. " -dither none -map req/mono.xpm -size 279x53 -pointsize 11 -gravity West".. " caption:'".. web.data[2].."\n"..web.data[6].."\n"..web.data[5].."\n"..web.data[3].. "'".. " temp/textBTM.xpm") -- create the text for advisories cmd.imageMGK(" -background black -fill white -font req/VeraMoBd.ttf".. " -dither none -map req/mono.xpm -size 139x64 -pointsize 11 -gravity Center".. " caption:'".. web.data[1].."'".. " temp/textALT.xpm") -- create the text for temperature cmd.imageMGK(" -background black -fill white -font req/VeraMoBd.ttf".. " -dither none -map req/mono.xpm -size 139x74 -pointsize 36 -gravity Center".. " caption:'".. web.data[4].."'".. " temp/textTMP.xpm") end text.convertIMG = function() local files = {"textTMP.xpm","textALT.xpm","textBTM.xpm"} local footer = {82,72,61} local width = {140,140,280} -- read the files into a table one at a time for img = 1, 3 do local file = io.open("temp/".. files[img],"r") table.insert(text.data.input, img, {}) for line in file:lines() do table.insert(text.data.input[img], tostring(line)) end file:close() -- remove header for y = 1, 7 do table.remove(text.data.input[img], 1) end -- remove footer table.remove(text.data.input[img], footer[img] - 7) -- remove non pixel data for y = 1, #text.data.input[img] do text.data.input[img][y] = string.sub(text.data.input[img][y], 2, width[img]) end end end text.sortIMG = function() local newChar = "" for img = 1, 3 do -- need to convert the strings into tables for y = 1, #text.data.input[img] do table.insert(text.data.apl2[img], {}) -- for each column in the current row for x = 1, #text.data.input[img][y] do -- read the character at that Y,X point newChar = string.sub(text.data.input[img][y], x,x) if newChar == "." then table.insert(text.data.apl2[img][y], x) -- pixel end end end end end text.packageIMG = function() for img = 1, 2 do for y = 1, #text.data.input[img] do local one = "" local two = "" local three = "" local four = "" for x = 1, #text.data.apl2[img][y] do if text.data.apl2[img][y][x] <= 35 then one = one .. string.sub(graphicsKey, text.data.apl2[img][y][x], text.data.apl2[img][y][x]) elseif text.data.apl2[img][y][x] <= 70 then two = two .. string.sub(graphicsKey, text.data.apl2[img][y][x] - 35, text.data.apl2[img][y][x] - 35) elseif text.data.apl2[img][y][x] <= 105 then three = three .. string.sub(graphicsKey, text.data.apl2[img][y][x] - 70, text.data.apl2[img][y][x] - 70) else four = four .. string.sub(graphicsKey, text.data.apl2[img][y][x] - 105, text.data.apl2[img][y][x] - 105) end end if one == "" and two == "" and three == "" and four == "" then table.insert(text.data.packed[img], "n") else table.insert(text.data.packed[img],one.."b"..two.."b"..three.."b"..four) end end end for y = 1, #text.data.input[3] do local one = "" local two = "" local three = "" local four = "" local five = "" local six = "" local seven = "" local eight = "" for x = 1, #text.data.apl2[3][y] do if text.data.apl2[3][y][x] <= 35 then one = one .. string.sub(graphicsKey, text.data.apl2[3][y][x], text.data.apl2[3][y][x]) elseif text.data.apl2[3][y][x] <= 70 then two = two .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 35, text.data.apl2[3][y][x] - 35) elseif text.data.apl2[3][y][x] <= 105 then three = three .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 70, text.data.apl2[3][y][x] - 70) elseif text.data.apl2[3][y][x] <= 140 then four = four .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 105, text.data.apl2[3][y][x] - 105) elseif text.data.apl2[3][y][x] <=175 then five = five .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 140, text.data.apl2[3][y][x] -140) elseif text.data.apl2[3][y][x] <= 210 then six = six .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 175, text.data.apl2[3][y][x] - 175) elseif text.data.apl2[3][y][x] <= 245 then seven = seven .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 210, text.data.apl2[3][y][x] - 210) else eight = eight .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 245, text.data.apl2[3][y][x] - 245) end end if one == "" and two == "" and three == "" and four == "" and five == "" and six == "" and seven == "" and eight == "" then table.insert(text.data.packed[3], "n") else table.insert(text.data.packed[3],one.."b"..two.."b"..three.."b"..four.."b"..five.."b"..six.."b"..seven.."b"..eight) end end end text.sendIMG = function() for img = 1, 3 do for y = 1, #text.data.packed[img] do cmd.sjinn(text.data.packed[img][y]) end cmd.sleep(2) end end
I start off in text.createIMG() by checking a couple things, One if there is any advisories, and if not place a No Advisories tag, Second looking for “windchill”, there is not always a “windchill” and if not place “dew point” in its place so we don’t have any blank lines. once were good to go we send the script off to imagemagick to make 3 graphics that contain black and white text. one for the long text at the bottom, one for advisories, and one for temperature.
text.convertIMG() reads the XPM files generated by imagemagick and does some cleanup. It starts by chopping the header and footer off of the image file, and removes the line formatting from each line of the image.
text.sortIMG() takes the leftover string data and scans each character in each line, in this case a white pixel is represented by a “.” (period) and a black pixel (which we don’t care about) is represented by a ” ” (space). Each time a period is found its x position is added to the end of a table. By the end of the image we are left with a table that has a subtable for each line, and contains X values for each pixel in each line, for example:
data= {}
data[1] = {1,2,12,80}
data[2] = {140,143,144,150}
There are 3 images to process, and they are different sizes, though how tall they are does not really matter to my script, its the width we are concerned about. text.packageIMG() reads each image and divides them up into blocks, the temperature text and advisory text are both 140 pixels wide and consume 4, 35 pixel blocks, so each value in each line is read out of our tables above and have some basic math done on them. If a value is greater than 35 for example then its block 2, the value has 35 subtracted from it and that is our block value (36 – 35 = block 2 pixel 1) . The bottom text is the widest graphic, taking up the entire width of the screen, but its just the same thing just spread over 8 blocks. Once we have our blocks as encoded strings they are packed into a single string per line of the image with “b” separating each block.
radar.lua does pretty much the same thing, except instead of making graphics it dithers the radar image downloaded earlier.
-- Weather Underground to Apple // -- 2011 Kevin Dady -- -- Radar processing: -- feed jpeg to image magick -- phrase output.xpm to color tables -- package for apple // -- send radar radar = {} radar.data = {} radar.data.input = {} -- raw file data table dump radar.data.packed = {} -- packed apple data radar.data.apl2 = {{},{},{},{},{},{}} -- black, green, violet, orange, blue, white radar.img = {} radar.img.header = 11 -- xpm file header # of lines radar.img.w = 141 radar.img.h = 141 radar.convertIMG = function() cmd.imageMGK(" temp/radar.jpg -level 0%,70%,1 -dither none -map req/apple.xpm temp/output.xpm") -- read the file into a table local file = io.open("temp/output.xpm","r") for line in file:lines() do table.insert(radar.data.input, tostring(line)) end file:close() -- remove header for i = 1, radar.img.header do -- hardcode table.remove(radar.data.input, 1) end -- remove footer table.remove(radar.data.input, radar.img.w) -- remove non color data for i = 1, #radar.data.input do radar.data.input[i] = string.sub(radar.data.input[i], 2, radar.img.h) end -- only deal with odd rows, due to the even / odd, bit / line, funny way apple 2's display highres colors. -- if we leave them all in the image there is a gret chance of 2 colors phasing into another, -- since we are going to loose pixel resolution anyway, we can cut that down some by deleting every other line -- giving 140x70 also making transfer size smaller. local keep ={} for i = 1, #radar.data.input do if (i % 2) == 1 then table.insert(keep, radar.data.input[i]) end end radar.data.input = keep end -- " " = 0 Apple color black (1) -- "X" = 1 Apple color green -- "o" = 2 Apple color violet -- "." = 5 Apple color orange -- "O" = 6 Apple color blue -- "+" = 7 Apple color white (2) radar.sortIMG = function() local newChar = "" -- need to convert the strings into tables for y = 1, #radar.data.input do -- add a new "line" string to each color table for color = 1, 6 do table.insert(radar.data.apl2[color], {}) end -- for each column in the current row for x = 1, #radar.data.input[y] do -- read the character at that Y,X point newChar = string.sub(radar.data.input[y], x,x) -- assign each character a individual table value if newChar == " " then table.insert(radar.data.apl2[1][y], x) -- black elseif newChar == "X" then table.insert(radar.data.apl2[4][y], x) -- GREEN elseif newChar == "o" then table.insert(radar.data.apl2[3][y], x) -- violet elseif newChar == "." then table.insert(radar.data.apl2[2][y], x) -- ORANGE elseif newChar == "O" then table.insert(radar.data.apl2[5][y], x) -- blue elseif newChar == "+" then table.insert(radar.data.apl2[6][y], x) -- white end end end end radar.packageIMG = function() for color = 1, 5 do -- ignore white, white takes a long time to draw since it makes up most of the graphic for y = 1, #radar.data.apl2[color] do local one = "" local two = "" local three = "" local four = "" for x = 1, #radar.data.apl2[color][y] do if radar.data.apl2[color][y][x] <= 35 then one = one .. string.sub(graphicsKey, radar.data.apl2[color][y][x], radar.data.apl2[color][y][x]) elseif radar.data.apl2[color][y][x] <= 70 then two = two .. string.sub(graphicsKey, radar.data.apl2[color][y][x] - 35, radar.data.apl2[color][y][x] - 35) elseif radar.data.apl2[color][y][x] <= 105 then three = three .. string.sub(graphicsKey, radar.data.apl2[color][y][x] - 70, radar.data.apl2[color][y][x] - 70) else four = four .. string.sub(graphicsKey, radar.data.apl2[color][y][x] - 105, radar.data.apl2[color][y][x] - 105) end end if one == "" and two == "" and three == "" and four == "" then table.insert(radar.data.packed, "n") else table.insert(radar.data.packed,one.."b"..two.."b"..three.."b"..four) end end end end radar.send = function() for line = 1, 350 do cmd.sjinn(radar.data.packed[line]) end end
Imagemagick is used to dither the radar image to 6 of the 8 “available” colors, the Apple II only has 6 unique colors in high resolution mode, and the other 2 are black 2 and white 2. This has to do with how the apple does color, I mentioned earlier that its a hack, which basically uses bit patterns and the phase of the color burst to generate different colors. The end effect is always interesting as you can never place a specific color on each and every location on each and every line.
In order to help cut down on artifacts between lines and to cut the data transfer in half I then only use the even lines of the image, which yea reduces my radar resolution from 140×140 to 140×70 but due to the above mentioned wonkyness of the Apple’s video system I would have lost most of that resolution anyway.
The output XPM file has its header, footer and line formatting removed and each color is split up into individual tables, from there the process is the same, reading each monochrome image, packing it into block defined lines and packed up for the Apple to consume. I ignore white and just draw that on the screen as it makes up most of the image, saving time, and the end data is 5 copies of the image line / block encoded, each acting as a mask for its unique color.
For the most part were done looking at the lua scripts, so join me for part 3 (the end) where we will explore the software the apple II uses.
It would be helpful if you put the link to part 1 in the text. Thanks.