Jacks or Better VIDEO POKER in 10 Lines of Atari BASIC

How do you cram Video Poker into 10 lines of 70’s era Atari BASIC?!
This all started Wednesday October 4th, 2023 when Y-Combinator Hacker News posted Show HN: Classic Video Poker(https://lfgslots.com/classicvideopoker/).

I like poker, especially 5 card games like Texas Hold Em’ and Classic Video Poker. I shared the link with a couple other veteran video game developers (Hi Brian & Kevin!), and conversation turned to how video poker should be programmed. This made me wonder if it could be coded in 10 lines of BASIC for the 2024 BASIC 10 Liner Contest .

The Rules

Jacks or Better VIDEO POKER is a 5 card draw poker game. The player places a bet, the deck is shuffled, and 5 cards are dealt. The player then selects which cards to hold and which to discard. Cards are discarded and replaced with new cards from the deck, the hand is evaluated, and if the player holds a winning hand they are rewarded with a payout based on the hand value and proportional to the initial bet. This process is repeated so long as the player has enough money to cover a bet, the player may add money to the game between hands.

The ranking and description of winning hands from highest to lowest:

  • Royal Flush Ten, Jack, Queen, King, Ace in the same suit
  • Straight Flush Five cards of consecutive values in the same suit
  • Four of a Kind Four cards of the same value
  • Full House Three of a kind and Two of a kind
  • Flush Any five cards in the same suit
  • Straight Any five cards of consecutive value, Ace may be low (A2345) or high (TJQKA) but may not wrap (not QKA23)
  • Three of a Kind Three cards of the same value
  • Two Pair Two pairs of cards of the same value
  • Jacks or better Two cards of the same value (“a pair”) that are Jack, Queen, King, or Ace

Cards do not need to be dealt in order in the hand (e.g. 7,8,4,6,5 is a straight equivalent to 4,5,6,7,8)

The Program

It was not easy. Here is the 2024 contest entry including the Atari floppy disk image and instructions: https://bunsen.itch.io/jacks-or-better-video-poker-atari-8-bit-by-d-scott-williamson

The 10 Line BASIC Program

1N0=0:N1=1:N2=N1+N1:N4=N2+2:N5=N4+N1:N9=N5+N4:N10=N5+N5:N13=N9+N4:N14=N13+N1:N15=N14+N1:N17=N15+N2:N19=N15+N4:N21=N19+N2:N25=N15+N10:N29=N25+N4:N119=119:N128=N119+N9:NKE=155:M=N25:B=N1:DIM D$(N119),HN$(165),H(N10+N10),H$(N10),T$(N128),C(N14),DC(N5)

2N7=N5+N2:N8=N7+N1:N16=N15+N1:F.I=N0 TO N19:REA.T:H(I)=T:N.I:O.#N1,N4,N0,"K":POK.82,N0:GR.N0:POK.752,1:SE.N2,N7,N2:SE.N1,N0,15:REA.D$:REA.T$:HN$=T$(N2):REA.T$:HN$(61)=T$(N2):?:F.I=N1 TO N15:?"        ";:N.I:POS.9,2:?"Ö É Ä Å Ï    Ð Ï Ë Å Ò"

3F.I=N9 TO N1 STEP -N1:T=N10-I:POS.N0,T+N4:?HN$(I*N15+N1,I*N15+N15):F.J=N1 TO N5:POS.N10+N1+N5*J,T+N4:?J*H(I+N10):N.J:N.I:?:?:?"Û±ÝÛ²ÝÛ³ÝÛ´ÝÛµÝ      Û­ÝÛ«Ý    Û¤Ý":POS.N0,N21:?,,,"  ÛÅÎÔÅÒÝ":?,,,"  Ä Å Á Ì";

4POS.N29,N19:?"Cash:$";M;" ":POS.N21,N19:?"Bet:$";B:GET#N1,K:B=B+(N1*(K=43)*(B<N5))-((K=45)*(B>1)):M=M+N25*(K=36):POS.N0,20:G.N4+(K=NKE)*(M>=B):D. 0,8,9,11,4,3,13,17,2,1, 0,1,2,3,4,6,9,25,50,250

5?HN$(150,164):?HN$(150,164):?HN$(N1,N15);HN$(N1,N15):F.I=N1 TO20STEP N2:J=INT(RND(N0)*52)*N2+N1:T$=D$(I):D$(I,I+N1)=D$(J):D.²€³€´€µ€¶€·€¸€¹€Ô€Ê€Ñ€Ë€Á€²à³à´àµà¶à·à¸à¹àÔàÊàÑàËàÁಐ³´µ¶·¸¹ÔÊÑËÁ²û³û´ûµû¶û·û¸û¹ûÔûÊûÑûËûÁûÁ²³´µ¶·¸¹ÔÊÑËÁ

6D$(J,J+N1)=T$:N.I:M=M-B:POS.N29,N19:?"Cash:$";M;" ":F.I=N1 TO N9 STEP N2:H$(I,I+N1)=D$(I):N.I: POS.N1,N19:F.I=N0 TO N5:DC(I)=N1:N.I:F.I=N1 TO N9 STEP N2: ?H$(I,I+N1);" ";:N.I:POS.N1,N17:D."               Jacks or BetterTwo Pair       Three of a Kind"

7F.I=N0 TO N4: DC(I)=DC(I)+(K=(I+49)):DC(I)=DC(I)-INT(DC(I)/N2)*N2:?CHR$(72-DC(I)*N4);"  ";:N.I: GET#N1,K: POS.N1,N17:G. N9-N2+(K=NKE): D."Straight       Flush          Full House     Four of a Kind Straight Flush Royal Flush                  "

8?HN$(N1,N13):F.I=N0 TO N4: J=I*N2+N1: T=J+DC(I)*N10:H$(J)=D$(T,T+N1):N.I:POS.N1,N19:F.I=N1 TO N9 STEP N2: ?H$(I,I+N1);" ";:N.I:?:F.I=N1 TO N14:C(I)=N0:N.I:R=N0: F.I=N1 TO N14:F.J=N1 TO N9 STEP N2:T=104+I:C(I)=C(I)+H$(J,J)=D$(T,T):N.J

9R=R+C(I)*C(I)*(I<N14): N.I:S=N0:F.I=N1 TO N10:T=N1:F.J=N0 TO N4:T=T*C(I+J):N.J:S=S+T:N.I:F=N1:F.I=N4 TO N10 STEP N2:F=F*H$(N2,N2)=H$(I,I):N.I:JB=N0:F.I=N10+N1 TO N14:JB=JB+(C(I)=N2):N.I:JB=JB*(R=N5+N2): R1=R+JB-S-F*2-(S*F*(C(N13))):HI=N0:W=N0:?:?

10F. I=N1 TO N10:HI=HI+I*(H(I)=R1):N.I:?" ";HN$(HI*N15+N1,HI*N15+N15);:T=H(HI+N10):F.I=N1 TO B:W=W+T:M=M+T:POS.N21,22:?"WIN $";W:POS.N29,N19:?"Cash:$";M:F.J=(T>N0)*(N16)TO N0 STEP-N4:SO.N1,N29,N10,J:SE.N2,N15-(J=N0)*N8,J+N2:N.J:SE.N2,N7,N2:N.I:G.N4

Program Overview

Line 1 Initialize constants, Dimension arrays

Line 2 Initialize constants, read hand ranks array from data, open keyboard device for key input, set left margin to 0, set graphics mode, hide the cursor, set colors, read deck of cards into D$ from data, read hand names from data into HN$, print white banner and VIDEO POKER title

Line 3 Display the winning hands and payout table, print legends for inputs

Line 4 Begin main game loop, pre-deal loop on line 4, print cash and bet, get input key in K, update bet B based on key if + or – pressed, update money (“Cash”) if $ pressed, GOTO 5 if ENTER pressed, otherwise GOTO 4 to continue pre-deal input loop, DATA statement with hand ranks and payouts

Line 5 Initial print of card bodies, clear previous hand and winning hand string from screen, shuffle the (first 10 cards of) deck D$, data containing initial deck of cards and card values for histogram

Line 6 Shuffle continued, subtract bet from cash to begin hand, update cash display, deal 5 cards by copying first 5 cards from the deck D$ to the hand H$, set 5 discard flags DC to 1, print cards in hand, position cursor to print hold flags, DATA containing hand name strings

Line 7 Discard input loop, loop through 5 cards, if a corresponding number key K is pressed toggle hold/discard flag in DC(I), print H or D based on DC(I), get key from keyboard in K, GOTO 7 loop until enter is pressed, DATA holding hand names

Line 8 Clear hold/discard characters, replace discarded cards with those from the deck, print cards in players hand, clear histogram C(), reset rank, loop over card values to calculate value counts of cards in the players hand to build a histogram

Line 9 Continue building histogram, and accumulate histogram values squared into initial rank R, detect straight, detect flush, detect jacks or better, combine rank, straight, flush, and jacks or better flags into a final rank R1 calculation, initialize the hand index and win to 0 in case the hand is not a winning hand

Line 10 Compare rank to winning hand ranks to get hand index HI, print hand name HN$(HI*15), lookup payout H(HI+10), payout loop once for each bet (1-5), award payout to win W and money M (“Cash”), print winnings and updated Cash balance, loop to cycle colors and ring bell, reset the screen color, GOTO 4 to return to the pre-deal state

The Program Expanded And Explained

Here each line number is multiplied by 100, command abbreviations are expanded and placed on separate lines with remarks.

100 N0=0        : REM CONSTANT VARIABLE 0 
101 N1=1        : REM CONSTANT VARIABLE 1
102 N2=N1+N1    : REM CONSTANT VARIABLE 2 
103 N4=N2+2     : REM CONSTANT VARIABLE 4 
104 N5=N4+N1    : REM CONSTANT VARIABLE 5 
105 N9=N5+N4    : REM CONSTANT VARIABLE 9 
106 N10=N5+N5   : REM CONSTANT VARIABLE 10 
107 N13=N9+N4   : REM CONSTANT VARIABLE 13 
108 N14=N13+N1  : REM CONSTANT VARIABLE 14 
109 N15=N14+N1  : REM CONSTANT VARIABLE 15 
110 N17=N15+N2  : REM CONSTANT VARIABLE 17 
111 N19=N15+N4  : REM CONSTANT VARIABLE 19 
112 N21=N19+N2  : REM CONSTANT VARIABLE 21 
113 N25=N15+N10 : REM CONSTANT VARIABLE 25 
114 N29=N25+N4  : REM CONSTANT VARIABLE 29 
115 N119=119    : REM CONSTANT VARIABLE 119 
116 N128=N119+N9: REM CONSTANT VARIABLE 128 
117 NKE=155     : REM CONSTANT VARIABLE 155 (KEYCODE FOR ENTER)
118 M=N25       : REM INITIALIZE MONEY TO 25 ($)
120 B=N1        : REM INITIALIZE BET TO 1 ($)
121 DIM D$(N119),HN$(165),H(N10+N10),H$(N10),T$(N128),C(N14),DC(N5) : REM DIMENSION VARIABLES
122 REM D$(1,52*2)  IS THE DECK OF CARDS, EACH CARD HAS A NUMBER AND A SUIT 
123 REM D$(105,119) ARE THE CARD NUMBERS FOR CALCULATING HISTOGRAMS A23456789TJKQA
124 REM HN$      IS THE HANDS NAME STRING, FOR DISPLAYING THE RATE TABLE AND HAND WHEN THE PLAYER WINS
125 REM H(20)    IS THE HAND VALUE TABLE, EACH OF THE 10 WINNING HANDS HAS A RANK AND A PAYOUT
126 REM H$(10)   IS THE PLAYERS 5 CARD HAND WHERE EVERY TWO CHARACTERS IS A CARD
127 REM T$(N128) IS A TEMPORARY STRING FOR READING STRINGS FROM DATA AND HOLDING STRINGS FOR CALCULATIONS
128 REM C(14)    HISTOGRAM OF CARD COUNTS IN PLAYERS HAND
129 REM DC(5)    DISCARD FLAG FOR DISCARDING OR HOLDING CARDS
 
200 N7=N5+N2    : REM CONSTANT VARIABLE 7 
201 N8=N7+N1    : REM CONSTANT VARIABLE 8 
202 N16=N15+N1  : REM CONSTANT VARIABLE 16 
203 FOR I=N0 TO N19 : REM LOOP TO READ HAND RANKS AND PAYOUTS FROM DATA INTO ARRAY H
204 READ T:?I,T            : REM READ DATA INTO TEMPORARY VARIABLE
205 H(I)=T            : REM COPY DATA INTO ARRAY (YOU CANNOT READ DATA DIRECTLY INTO AN ARRAY VARIABLE
206 NEXT I            : REM LOOP
207 OPEN #N1,N4,N0,"K": REM OPEN THE KEYBOARD FOR INPUT SO KEYSTROKES CAN BE READ WITHOUT REQUIRING ENTER KEY
208 POKE 82,N0        : REM SET LEFT MARGIN TO 0 FROM DEFAULT OF 2 TO USE ENTIRE SCREEN WIDTH
209 GRAPHICS N0       : REM GRAPHICS 0, CLEAR SCREEN AND SET ATARI 40 COLUMN 24 ROW TEXT SCREEN MODE
210 POKE 752,1        : REM HIDE CURSOR
211 SETCOLOR N2,N7,N2 : REM SET THE BACKGROUND COLOR TO DARK BLUE
212 SETCOLOR N1,N0,15 : REM SET THE TEXT COLOR TO BRIGHT WHITE
213 READ D$           : REM READ THE DECK OF CARDS STRING FROM DATA
214 READ T$           : REM READ PARTIAL HAND NAMES FROM DATA
215 HN$=T$(N2)        : REM COPY THE HAND NAMES STRING SKIPPING THE INITIAL DOUBLE QUOTE CHARACTER IN T$
216 READ T$           : REM READ MORE OF THE HAND NAMES FROM THE NEXT DATA STATEMENT
217 HN$(61)=T$(N2)    : REM COPY THE HAND NAMES TO THE END OF HN$ SKIPPING THE INITIAL DOUBLE QUOTE CHARACTER IN T$
218 PRINT             : REM PRINT A BLANK LINE 
219 FOR I=N1 TO N15   : REM LOOP TO PRINT WHITE BANNER AT THE TOP OF THE SCREEN 
220 PRINT "        "; : REM PRINT 8 INVERTED SPACES
221 NEXT I            : REM LOOP
222 POSITION 9,2                  : REM POSITION CURSOR IN MIDDLE OF THE BANNER
223 PRINT "Ö É Ä Å Ï    Ð Ï Ë Å Ò": REM PRINT INVERTED "V I D E O   P O K E R"

300 FOR I=N9 TO N1 STEP -N1       : REM LOOP TO DISPLAY THE WINNING HANDS AND PAYOUTS FOR BETS
301 T=N10-I                       : REM TEMPORARY VARIABLE T INDEXES THE ROW POSITION ON THE SCREEN IN DESCENDING ORDER
302 POSITION N0,T+N4              : REM POSITION THE CURSOR TO PRINT THE NAME OF THE HAND
303 PRINT HN$(I*N15+N1,I*N15+N15) : REM PRINT HAND NAME SUBSTRING OUT OF HN$ INDEXED BY I.  EACH NAME IS 15 CHARACTERS LONG
304 FOR J=N1 TO N5                : REM LOOP OVER THE BET AMOUNTS FROM 1 TO 5
305 POSITION N10+N1+N5*J,T+N4     : REM POSITION THE CURSOR IN THE RIGHT COLUMN AND ROW TO DISPLAY THE PAYOUT FOR HAND I AT BET LEVEL J
306 PRINT J*H(I+N10)              : REM PRINT PAYOUT FOR HAND I AT BET LEVEL J
307 NEXT J                        : REM LOOP OVER BET LEVELS
308 NEXT I                        : REM LOOP OVER HANDS
309 PRINT                         : REM PRINT A BLANK LINE
310 PRINT                         : REM PRINT A BLANK LINE
311 PRINT "Û±ÝÛ²ÝÛ³ÝÛ´ÝÛµÝ      Û­ÝÛ«Ý    Û¤Ý" : REM PRINT KEY LEGEND, INVERTED "[1][2][3][4][5]   [-][+]   [$]"
312 POSITION N0,N21               : REM POSITION CURSOR TO PRINT DEAL KEY LEGEND
313 PRINT ,,,"  ÛÅÎÔÅÒÝ"          : REM PRINT INVERTED "[ENTER]" TO THE RIGHT
314 PRINT ,,,"  Ä Å Á Ì";         : REM PRINT INVERTED "D E A L" TO THE RIGHT, SEMICOLON PREVENTS NEWLINE FROM SCROLLING THE SCREEN
    
400 POSITION N29,N19              : REM THIS IS THE BET AND CASH INPUT LOOP, POSITION THE CURSOR TO PRINT PLAYER CASH
401 PRINT "Cash:$";M;" "          : REM PRINT THE AMOUNT OF MONEY THE PLAYER HAS, TRAILING SPACE NEEDED TO CLEAR DIGITS AS PLAYER CASH DECREASES
402 POSITION N21,N19              : REM POSITION THE CURSOR TO PRINT THE PLAYER BET 
403 PRINT "Bet:$";B               : REM PRINT THE PLAYER BET LEVEL
404 GET#N1,K                      : REM READ A KEY FROM THE KEYBOARD INTO K
405 B=B+(N1*(K=43)*(B<N5))-((K=45)*(B>1)) : REM UPDATE BET, ADD 1 ONLY IF K=43 ("+") AND THE BET IS LESS THAN 5, SUBTRACT 1 IF K=45 ("-") AND B>1
406 M=M+N25*(K=36)                : REM UPDATE PLAYER MONEY, ADD 25 ONLY IF THE "$" KEY WAS PRESSED WHEN K=36
407 POSITION N0,20                : REM POSITION CURSOR TO DISPLAY CARDS
408 GOTO 100*(N4+(K=NKE)*(M>=B))  : REM GOTO N4+(K=NKE)*(M>=B), GOTO IF ENTER PRESSED AND MONEY>=BET GOTO 5 (DEAL), OTHERWISE GOTO 4 (LOOP)
409 DATA 0,8,9,11,4,3,13,17,2,1, 0,1,2,3,4,6,9,25,50,250
410 REM DATA ON LINE 409 IS 10 HAND RANKS FOLLOWED BY 10 CORRESPONDING HAND VALUES
    
500 PRINT HN$(150,164)              : REM PRINT FIVE PAIRS OF INVERTED SPACES TO LOOK LIKE CARDS FROM THE END OF THE HAND NAMES STRING
501 PRINT HN$(150,164)            : REM PRINT FIVE PAIRS OF INVERTED SPACES TO LOOK LIKE CARDS FROM THE END OF THE HAND NAMES STRING
502 PRINT HN$(N1,N15);HN$(N1,N15) : REM PRINT 15 BLANK SPACES TWICE TO CLEAR THE HAND AND "WIN$:X" STRINGS FROM THE SCREEN (THE FIRST HAND NAME IS BLANK, USED FOR ALL LOSING HANDS)
503 FOR I=N1 TO 20 STEP N2        : REM SHUFFLE THE DECK, ONLY THE FIRST 10 CARDS AS THEY ARE THE ONLY ONES THAT MAY BE USED IN EACH HAND
504 J=INT(RND(N0)*52)*N2+N1       : REM PICK ANOTHER RANDOM CARD IN THE DECK INDEXED BY J TO SWAP WITH CARD AT INDEX I
505 T$=D$(I)                      : REM COPY THE DECK AT I TO T$
506 D$(I,I+N1)=D$(J)              : REM COPY THE 2 CHARACTER CARD FROM LOCATION J IN THE DECK TO LOCATION I
507 DATA²€³€´€µ€¶€·€¸€¹€Ô€Ê€Ñ€Ë€Á€²à³à´àµà¶à·à¸à¹àÔàÊàÑàËàÁಐ³´µ¶·¸¹ÔÊÑËÁ²û³û´ûµû¶û·û¸û¹ûÔûÊûÑûËûÁûÁ²³´µ¶·¸¹ÔÊÑËÁ 
508 REM THE DATA IN LINE 507 IS THE INVERTED CHARACTERS OF THE DECK OF CARDS FOLLOWED BY VALUES OF CARDS (A123456789TJQKA) USED TO CALCULATE RANKING HISTOGRAM
    
600 D$(J,J+N1)=T$                 : REM COPY THE TWO CHARACTER CARD FROM POSITION I SAVED IN T$ TO LOCATION J
601 NEXT I                        : REM LOOP TO SHUFFLE NEXT CARD
602 M=M-B                         : REM SUBTRACT BET FROM MONEY
603 POSITION N29,N19              : REM POSITION CURSOR TO PRINT PLAYER CASH
604 PRINT "Cash:$";M;" "          : REM PRINT PLAYER CASH
605 FOR I=N1 TO N9 STEP N2        : REM LOOP TO DEAL 5 CARDS
606 H$(I,I+N1)=D$(I)              : REM COPY THE FIRST 5 CARDS FROM THE DECK INTO THE PLAYERS HAND 2 CHARACTERS AT A TIME 
607 NEXT I                        : REM LOOP DEAL
608 POSITION N1,N19               : REM POSITION CURSOR TO PRINT CARDS
609 FOR I=N0 TO N5                : REM LOOP TO SET ALL 5 DISCARD FLAGS TO 1
610 DC(I)=N1                      : REM SET DISCARD FLAG I TO 1
611 NEXT I                        : REM LOOP INITIALIZE DISCARD FLAGS
612 FOR I=N1 TO N9 STEP N2        : REM LOOP TO PRINT CARDS
613 PRINT H$(I,I+N1);" ";         : REM PRINT 2 CHARACTER CARD FROM THE HAND FOLLOWED BY A SPACE
614 NEXT I                        : REM LOOP TO PRINT CARDS
615 POSITION N1,N17               : REM POSITION CURSOR TO PRINT DISCARD/HOLD FLAGS
616 DATA"               Jacks or BetterTwo Pair       Three of a Kind" : REM FIRST DATA FOR HN$
    
700 FOR I=N0 TO N4                : REM DISCARD/HOLD INPUT LOOP, LOOP THROUGH 5 CARD DISCARD/HOLD VALUES
701 DC(I)=DC(I)+(K=(I+49))        : REM INCREMENT D(I) DISCARD/HOLD IF K=49+I, KEYS 1-5 ARE KEY CODES 49-53
702 DC(I)=DC(I)-INT(DC(I)/N2)*N2  : REM EFFECTIVELY MODULUS 2, LIMITS THE VALUES TO 1 OR 0
703 PRINT CHR$(72-DC(I)*N4);"  "; : REM PRINT "D" IF DC(I) IS 1 OR "H" IF IT IS NOT FOLLOWED BY TWO SPACES
704 NEXT I                        : REM LOOP THROUGH DISCARD ARRAY
705 GET #N1,K                     : REM GET KEY FROM KEYBOARD
706 POSITION N1,N17               : REM POSITION CURSOR TO PRINT DISCARD VALUES
707 GOTO 100*(N9-N2+(K=NKE))      : REM GOTO N9-N2+(K=NKE), IF ENTER PRESSED GOTO 800 OTHERWISE GOTO 700
708 DATA"Straight       Flush          Full House     Four of a Kind Straight Flush Royal Flush                  " : REM SECOND DATA FOR HN$

800 PRINT HN$(N1,N13)             : REM PRINT BLANK HAND NAME OVER DISCARD VALUES TO REMOVE THEM
801 FOR I=N0 TO N4                : REM LOOP THROUGH 5 CARDS TO DEAL NEW CARDS FOR DISCARDS
802 J=I*N2+N1                     : REM CALCULATE INDEX j OF CARD I IN THE HAND H$ (AND IN THE DECK D$ ON DEAL)
803 T=J+DC(I)*N10                 : REM CALCULATE INDEX OF THE CARD IN THE DECK, IF DISCARD IS 1, SELECT CORRESPONDING CARD FROM NEXT 5 CARDS IN THE DECK
804 H$(J)=D$(T,T+N1)              : REM COPY CARD FROM THE DECK TO THE HAND (EITHER HOLD OR DISCARD AND REDEAL CARD+5)
805 NEXT I                        : REM LOOP DEAL DISCARDS
806 POSITION N1,N19               : REM POSITION CURSOR TO PRINT CARDS
807 FOR I=N1 TO N9 STEP N2        : REM LOOP OVER CARDS TO PRINT CARDS
808 PRINT H$(I,I+N1);" ";         : REM PRINT 2 CHARACTER CARD FROM HAND AT INDEX I
809 NEXT I                        : REM LOOP
810 PRINT                         : REM PRINT BLANK LINE IN PREPARATION FOR PRINTING HAND DESCRIPTION AND WINNING
811 FOR I=N1 TO N14               : REM LOOP OVER CARD VALUES TO CLEAR HISTOGRAM
812 C(I)=N0                       : REM SET HISTOGRAM VALUE C(I) TO ZERO
813 NEXT I                        : REM LOOP
814 R=N0                          : REM SET RANK TO 0
815 FOR I=N1 TO N14               : REM LOOP I OVER CARD VALUES TO CALCULATE HISTOGRAM IN C()
816 FOR J=N1 TO N9 STEP N2        : REM LOOP J OVER CARD INDEXES IN HAND
817 T=104+I                       : REM SET TEMPORARY VALUE TO 104+I,INDEX OF VALUE CHARACTER IN DECK STRING
818 C(I)=C(I)+H$(J,J)=D$(T,T)     : REM ADD 1 TO C(I) IF THE VALUE CHARACTER IN THE HAND FOR CARD J MATCHES THE VALUE CHARACTER FROM THE DECK STRING
819 NEXT J                        : REM LOOP NEXT CARD
    
900 R=R+C(I)*C(I)*(I<N14)         : REM RANK IS THE SUM OF THE CARD COUNTS SQUARED ONLY INCLUDING THE ACE ONCE (IT'S INCLUDED TWICE TO DETECT STRAIGHTS)
901 NEXT I                        : REM NEXT CARD VALUE, R CONTAINS RANK SUM OF SQUARES OF CARD COUNTS FOR THE HAND 
902 S=N0                          : REM SET STRAIGHT FLAG S TO ZERO
903 FOR I=N1 TO N10               : REM LOOP OVER LOWEST CARD FOR 10 POSSIBLE STRAIGHTS (A-5 THROUGH T-A)
904 T=N1                          : REM SET TEMP TO 1
905 FOR J=N0 TO N4                : REM LOOP THROUGH 5 CONSECUTIVE CARD COUNTS C(I+J) IN THE HISTOGRAM
906 T=T*C(I+J)                    : REM MULTIPLY T BY THE HISTOGRAM VALUE AT I+J
907 NEXT J                        : REM LOOP NEXT CARD
908 S=S+T                         : REM ADD T TO S, T CAN ONLY BE 1 IF THE HAND CONTAINS 5 CONSECUTIVE CARDS
909 NEXT I                        : REM LOOP NEXT POTENTIAL STRAIGHT
910 F=N1                          : REM SET FLUSH DETECTION TO 1
911 FOR I=N4 TO N10 STEP N2       : REM LOOP HAND INDEX I FROM THE SECOND CARD SUIT TO THE FIFTH CARD SUIT 
912 F=F*H$(N2,N2)=H$(I,I)         : REM MULTIPLY THE FLUSH FLAG BY ONE ONLY IF THE SUIT CHARACTER OF THE FIRST CARD MATCHES THE SUIT OF THE INDEXED CARD
913 NEXT I                        : REM NEXT CARD SUIT INDEX, WHEN DONE F WILL ONLY BE 1 IF ALL CARDS ARE THE SAME SUIT
914 JB=N0                         : REM SET JACKS OR BETTER FLAG TO 0
915 FOR I=N10+N1 TO N14           : REM LOOP INDEX I THROUGH THE CARD HISTOGRAMS FOR JACK (11) THROUGH HIGH ACE (14)
916 JB=JB+(C(I)=N2)               : REM ADD 1 TO JB ONLY IF THE HISTOGRAM FOR VALUE I IS EQUAL TO 2
917 NEXT I                        : REM LOOP THROUGH JACKS OR BETTER CARD COUNTS
918 JB=JB*(R=N5+N2)               : REM JB IS MULTIPLIED BY 1 ONLY IF THE RANK IS 7, OTHERWISE 0.  RANK 7 IS A SINGLE PAIR (2^2+3*1^2)
919 R1=R+JB-S-F*2-(S*F*(C(N13)))  : REM R1 IS THE FINAL COMPOSITE HAND RANK INCORPORATING FLUSH, STRAIGHT, JACKS OR BETTER FLAG, AND DETECTION OF ROYAL FLUSH (S*F*(C(N13)))
920 HI=N0                         : REM SET THE HAND INDEX HI TO ZERO
921 W=N0                          : REM WIN AMOUNT TO ZERO 
922 PRINT                         : REM PRINT BLANK LINE IN PREPARATION FOR PRINTING HAND DESCRIPTION AND WINNING
923 PRINT                         : REM PRINT BLANK LINE IN PREPARATION FOR PRINTING HAND DESCRIPTION AND WINNING

1000 FOR I=N1 TO N10              : REM LOOP THROUGH THE POTENTIAL WINNING HANDS (INDEX 1 - 10)
1001 HI=HI+I*(H(I)=R1)            : REM ADD WINNING HAND INDEX I TO HI ONLY IF THE HAND RANK EQUALS THE HAND RANK AT THE INDEX H(I)
1002 NEXT I                       : REM LOOP NEXT WINNING HAND, WHEN THIS IS DONE HI WILL CONTAIN AN INDEX INTO HAND NAMES AND PAYOUTS 
1003 PRINT " ";HN$(HI*N15+N1,HI*N15+N15); : REM PRINT HAND NAME, THE NAME AT INDEX ZERO IS BLANK FOR NON-WINNING HANDS
1004 T=H(HI+N10)                  : REM COPY THE WINNING BASE PAYOUT TO T
1005 FOR I=N1 TO B                : REM LOOP FROM 1 TO BET, HIGHER BETS, MORE WINNING PIZAZZ
1006 W=W+T                        : REM WITH EACH ITERATION OF THE LOOP THE WIN INDICATOR IS AWARDED THE BASE PAYOUT
1007 M=M+T                        : REM WITH EACH ITERATION OF THE LOOP THE PLAYERS MONEY IS INCREASED BY T
1008 POSITION N21,22              : REM POSITION CURSOR TO PRINT PLAYER WINNINGS
1009 PRINT "WIN $";W              : REM PRINT WINNINGS
1010 POSITION N29,N19             : REM POSITION CURSOR TO PRINT PLAYER MONEY
1011 PRINT "Cash:$";M             : REM PRINT PLAYER MONEY
1012 FOR J=(T>N0)*(N16)TO N0 STEP-N4 : REM IF THE WINNING BASE PAYOUT IS NOT ZERO, LOOP FROM 16 TO 0 
1013 SOUND N1,N29,N10,J           : REM MAKE WINNING SOUND
1014 SETCOLOR N2,N15-(J=N0)*N8,J+N2 : REM SET SCREEN TO EXCITING WINNING COLOR
1015 NEXT J                       : REM END OF MULTIMEDIA REWARD LOOP (END OF FLASH AND DING)
1016 SETCOLOR N2,N7,N2            : REM RESTORE SCREEN COLOR
1017 NEXT I                       : REM LOOP BET AMOUNT TIMES
1018 GOTO 100*N4:REM GOTO N4      : GO TO BET, MONEY, AND DEAL INPUT LOOP LINE

Programming techniques

The deck of cards

The program manages a deck of 52 cards in a D$ where each card is represented by a value character (2-9,T,J,Q,K,A) and suit character (hearts, clubs, diamonds, spades). Generating cards randomly risks dealing duplicate cards and checking for duplicates is more work than managing a deck. The initial deck is read into D$ from data in line 2. The deck is shuffled in a loop spanning lines 5 and 6 by swapping each card with a random one in the deck. Initially, the program shuffled all 52 cards but since the deck is shuffled between each deal and each hand can only use a maximum of the first 10 cards (5 cards discarded and re-dealt) it is only necessary to shuffle the first 10 cards of the deck between hands reducing the delay considerably.

Ranking hands

This is the heart of this program, it accurately ranks a Jacks or Better Poker hand in about 1 line of code spanning 2 lines of this program. The ranking system must accurately rank hands according to many rules and be compact enough to fit in the 10 line program. IF … THEN logic would quickly consume too many lines so I needed to devise a ranking system. The hand ranks from highest to lowest are:

  • Royal Flush : Ten, Jack, Queen, King, Ace in the same suit
  • Straight Flush : Five cards in a row in the same suit
  • Four of a Kind : Four cards of the same value
  • Full House : Three of a kind and Two of a kind
  • Flush : Any five cards in the same suit
  • Straight : Any five cards in order, Ace may be low (A2345) or high (TJQKA) but may not wrap (not QKA23)
  • Three of a Kind : Three cards of the same value
  • Two Pair : Two pairs of any cards of the same value
  • Jacks or better : Two cards of the same value that are Jack, Queen, King, or Ace

Some combination of rules like a “Royal Straight” Ten, Jack, Queen, King, Ace are not legal Poker Hands and must not fall out of the hand ranking system.
To rank a hand several calculations are made and flags are set then combined to determine a numerical rank used to lookup hand values from a table.
Variables used in ranking:

  • R Initial Rank
  • R1 Final Rank
  • S Straight detection (1 if there are 5 sequential card values in the hand in any order)
  • F Flush detection (1 if all 5 cards in the hand are the same suit)
  • JB Jacks or Better detection (1 only if there is a single pair in the hand (rank 7) and the pair is Jack, Queen, King, or ACE (a 2 in the histogram for one of those values)
  • C(14) Counts (histogram) of cards A123456789TJQKA (includes A at both ends for straight detection)

Steps in the calculation

  • Count the number of cards of each value in the hand into array C to create the histogram (line 8-9). A is counted twice, once at each end for straight calculation later.
  • The sum of the squares of the histogram values (line 9) for the first 13 histogram values (so A is not included twice) is accumulated in the initial rank R. At this point the rank values have the following meanings:
    • 17 (4*4)+1 Four of a kind
    • 13 (3*3)+(2*2) Full house
    • 11 (3*3)+1+1 Three of a kind
    • 9 (2*2)+(2*2)+1 Two pair
    • 7 (2*2)+1+1+1 Pair
    • 5 1+1+1+1+1 Five different value cards
  • Search for straights (line 9): Use a nested loop 0-10 then 0-4 to multiply consecutive card counts in the histogram A straight will be 5 ones in a row so S can only be 1 if there are 5 cards in a row in any order in the hand.
  • Search for flush (line 9) by comparing the suit of the last 4 cards in the hand to the suit of the first and take the product of the comparisons. It can only be 1 if all cards are the same suit.
  • Search for Jacks or Better JB (line 9) loop through counts of Jack, Queen, King, and Ace and add 1 to JB if the count is 2, then keep the result only if the initial rank is 7 (single pair). JB is only a 1 if the hand contains a single pair of Jacks or better.
  • The final rank calculation (line 9) combines R with JB, S, F, and the count of Aces (C(13)) as follows R1=R+JB-S-F*2-(S*F*(C(N13))) resulting in the final hand ranking. Values have the following meanings:
    • 17 (4*4)+1 Four of a kind
    • 13 (3*3)+(2*2) Full house
    • 11 (3*3)+1+1 Three of a kind
    • 9 (2*2)+(2*2)+1 Two pair
    • 8 7+JB Jacks or better
    • 7 (2*2)+1+1+1 Pair (use jacks or better)
    • 5 1+1+1+1+1 Nothing
    • 4 5-S Straight
    • 3 5-2*F flush
    • 2 5-S-2*F Straight flush
    • 1 5-S-2*F-(S*F*(C(N13))) Royal flush
  • The winning hand values are stored in the table with the payouts. A loop scans all the potential winning hands to see if the rank matches one of the hands (line 10) and if so lookup the corresponding payout

Payout

If the player wins then the payout loop flashes the screen and rings the bell once for each dollar bet and adds the payout amount to the players total cash each time.

Audio Visuals

Just like in real gambling machines, wins are accompanied by ringing bells and flashing lights while losses pass silently leaving only reward and anticipation to drive continued play.

Input method

There are too many options to use the joystick and it would have been unintuitive and using BASIC’s INPUT requires pressing enter after every input and validating inputs so I decided to access the keyboard device to get blocking input on individual key-presses. The keyboard device is opened for input in line 2 O.#N1,N4,N0,"K" (OPEN #1,4,0,"K") then keys are retrieved in input loops in line 4 and 7 GET#N1,K (GET #1,K). The GET statement waits for a key and returns the ATASCII value in K which is used in conditionals in assignment and GOTO statements. Examples include pressing $ to add money to the player account M=M+N25*(K=36) which adds 25 if K= 36 the ATASCII code for $ and 0 otherwise or G.N4+(K=NKE)*(M>=B) which loops at line 4 until ENTER (ATASCII 155 assigned to NKE for number keyboard enter) is pressed and the player has enough money to cover their bet.

Use of conditional expressions as integers (booleans)

Atari BASIC has no ELSE, so everything to the right of THEN (on one of only 10 BASIC program lines) is only executed if the IF clause is true placing a hard limit on the amount and structure of logic possible in a short program. The result of a comparison (=,<,<=,>,>=,<>) is evaluated to 1 if true and 0 if false. This can be used to create conditional calculations in without using IF … THEN. This technique is used extensively in this program to handle input, make conditional calculations in hand ranking, and in GOTO statements to vector to intended lines based on conditions.

Use of variables for constants

From the Atari BASIC Manual: “Each time a constant (4,5,16,3.14159, etc.) is used, it takes 7 bytes. Defining a new variable requires 8 bytes plus the length of the variable name (in characters). But each time it is used after being defined, it takes only I byte, regardless of its length.
Lines that contain too many constants overflow the internal tokenization buffer and cannot use the maximum 253 characters per line permitted by the Atari BASIC input buffer length. Using variables for constants is much more compact, furthermore deriving constants from other constants is even more compact during initialization.

Modulus operator

The following modulus 2 calculation is used in line 5 to toggle holding cards:
DC(I)=DC(I)-INT(DC(I)/N2)*N2
Atari BASIC does not have a modulus operator, modulus(a,b) returns the remainder of a/b
Instead A-INT(A/B)*B is used, let’s look at it in parts.
INT(A/B) is the integer portion of A/B, aka truncated value of A/B, aka ceil(A/B)
INT(A/B)*B is the integer portion of A larger than
B A-INT(A/B)*B Subtracting the integer portion of A larger than B from A leaves the remainder or modulus.

Conclusion

I love these highly constrained programming challenges and I think Jacks or Better VIDEO POKER turned out great. It’s certainly been fun play-testing it! I think this style of technical programming is probably my favorite part of developing games. Working with Atari, especially Atari BASIC, is a special kind of nostalgia for me that always brings me back to my programming and literal adolescence. I hope you found this entertaining, wish me luck! Contest results to be announced April 6th 2024.

PCBWay Sponsors Workshop 88 Makerspace

We’d like to welcome Workshop 88‘s newest sponsor: PCBWay!

PCBWay provides PCB Manufacturing, assembly, CNC, 3D Printing services, and more.

We’d like to thank PCBWay for helping Workshop 88 makerspace achieve our mission of providing tools and technology access to makers. Watch for exciting new Workshop 88 projects using PCBWay products and services.

If you’d like to sponsor or support Workshop 88 makerspace, send us an email at info@workshop88.com

Revisiting the Art Institute of Chicago Project

I’d written previously about my project to display images of artworks owned by the Art Institute of Chicago (ARTIC) https://workshop88.com/oldblog/index.php/2023/01/14/the-adafruit-pyportal-and-modifying-adafruit-libraries/. The challenge was that, unlike the project that inspired it, the ARTIC’s REST API does not return the full URL path to the image files. I solved it by patching the source code for the PyPortal library. It was clunky and stopped me from using that PyPortal for any other project.

Aha!

Then, inspiration struck. I had an under utilized Raspberry Pi W and decided to implement a web server on it that could take the response from the ARTIC, massage it and forward it on to the PyPortal. So rather than talking directly to ARTIC, the PyPortal talks to the Raspi, which talks to ARTIC. I used Bottle to build the web server.

Enhance the original

To spice up this second version, I implemented a “Happy Birthday, <artist>” system. The PyPortal asks the Raspi for the URL of the nth image to be displayed. The Raspi looks up which artists have birthdays today and asks ARTIC if it owns any pieces by those artists. From that search results list, the Raspi constructs the URL to the nth image. It puts the image URL, the artist’s name, the artwork’s title, and the dimensions of the image into a dictionary structure and sends a JSON response back to the PyPortal. The PyPortal downloads the image and displays it on the screen.

New things to learn

For this project to work, Raspi had to compose a query for ARTIC that returned only pieces by the artists desired. It would be poor form to display Pearson’s works on Beardson’s birthday. The prior project didn’t require such high precision. “/artworks/search/?q=impressionism” was adequate to get a lot of very beautiful imagess. For greater precision, I was going to have to learn complex queries using Elasticsearch’s Query DSL. The engineering staff at the ARTIC was very responsive in providing support, but I still struggled. I eventually found an on-line course on Udemy.com for the complete Elasticsearch stack. Section 3 of the course was DSL. I signed up for a free trial and completed the training.

Learning DSL allowed me to develop the following process:

  1. PyPortal request to Raspi: Get the nth image’s URL
  2. Raspi table lookup: What artists were born today?
  3. Raspi request to ARTIC: Perform a query to get those artists’ ARTIC IDs
  4. Raspi to ARTIC: Perform a query to get the URL of the nth image by those artists
  5. Raspi to PyPortal: Massage ARTIC’s response and send the modified image URL to PyPortal
  6. PyPortal: Download and display the image
  7. Rinse/Repeat

Here is what the Raspi uses to find artists IDs:

criteria = {
 "query": {
  "bool": {
   "should": [
    {"match_phrase":{"title":     {"query":artist_name,"slop":1}}},
    {"match_phrase":{"alt_titles":{"query":artist_name,"slop":1}}},
    ], # should
   "minimum_should_match": 1,
  }, # bool
 }, # query
} # criteria

The query takes the artist’s name and tries to find that name in the title field. It also looks for that name in the alt_titles fields. The ‘slop:1’ specifies that ‘Louis Sullivan’ (the name in the DailyArtFixx.com birthday database) is a close enough match to ‘Louis H. Sullivan’ (the name used by the ARTIC).

The alt_titles field is necessary because the artist Maurice Quentin de La Tour (DailyArtFixx.com) has all of the following aliases:

      "Maurice Q. de Latour",
      "Maurice Quentin La Tour",
      "Maurice-Quentin de La Tour",
      "Maurice-Quentin de La Tour",
      "Maurice-Quentin de la Tour",
      "Maurice Quentin De La Tour",
      "Maurice Quentin Delatour",
      "Maurice Quentin de Latour",
      "Maurice Quentin de La Tour"

Misspellings are also a risk. Taddeo Zuccari is also known as Taddeo Zuccaro. Hopefully, the alt_titles will catch this. If it doesn’t, there is still a “fuzziness” factor that can be added to the query. Fuzziness can handle individual letter insertions, deletions and transpositions. The greater the fuzziness factor, the more errors it can tolerate — at the risk of including someone who should not be included. Combining fuzziness and slop makes for a very complex query and since alt_titles seems to be working, I only use 1 degree of slop and skip fuzziness.

If Raspi is successful finding artist_name, it extracts that artist’s unique ARTIC ID. I do the above query for each artist having a birthday today. I put all the found IDs into a list.

I then loop over all the IDs and build something like the following query to find all the ARTIC artworks. The filter clauses ensure that the ARTIC has an image of the artwork and that there is a title and artist name associated to it. In the following example, I use IDs to find all artworks by two artists — Maurice LaTour and Casper Friedrich.

criteria = {"query": 
   {"bool": {	
      "should": [
         {"match": {"artist_id": 34563}},
         {"match": {"artist_id": 34185}}],
      "minimum_should_match": 1,
      "filter": [
         {"exists": {"field" : "image_id"}},
         {"exists": {"field" : "artist_title"}}, 
         {"exists": {"field" : "title"}}, ], }, }, } 

Every time the PyPortal asks for the nth image, the Raspi sends back data from the nth image in this list.

While there are plenty of opportunities for refactoring and bullet proofing the code, I consider the project complete and a success. It is running continuously in my workshop — Happy Birthday, artists!

Final thoughts

The brains of the project were moved from the PyPortal into the Bottle web server. I can change the server and add any number of parallel query schemes — all the works in the Narcissa Niblack Thorne miniatures gallery, all artwork associated with the Caravaggio exhibit, images of cats, stained glass, architectural models, etc. What would you like to see?

Fireworks Deck Umbrella: Have a Safe and Happy Fourth of July

When I was first learning about microcontrollers and blinking lights, I’d seen a YouTube video by Dave Plummer, a retired system engineer at Microsoft (he wrote Task Manager). In that video, he creates what he called a Tiki Umbrella. It’s a deck umbrella tricked out with LED strips along the ribs that produce various animation effects.

I thought that would be a nice project to do for the 4th of July.

My deck umbrella has eight ribs — so, an LED strip for each rib. My original plan was to have a ninth strip running up the pole — to mimic the launch of the firework shell into the air before it explodes.

As with many of my projects, I procrastinated and ran short of time. July 4th was a hard stop date. I also lacked inventory on all the appropriate components. So the project that I implemented was not all that I envisioned.

Because the LED strips would be somewhat sheltered from the elements under the umbrella, I didn’t feel the need for IP65 or higher (IP stands for ingress protection). I settled on two roles of Inland brand, 5-meter, WS2812B, 60 LEDs/meter strips. Given the length of the umbrella ribs, this works out to 76 pixels per rib.

For the microcontroller, I used an RP2040 Scorpio, running CircuitPython and the Adafruit LED Animation library. Here’s the wiring diagram.

Rather than a breadboard for an outdoor project, I decided to swallow my fear of soldering and used a permanent proto-board as the diagram suggests. That’s about 50 solders. Face your fear and push through it, MACE! Surprisingly, it was much easier that I’d expected. Flux and good, Pb solder makes a big difference.

I’d done all the soldering on my workbench and tested the animation software as I went — solder a strip, test the software, rinse, repeat.

Once all the strips were working, it was time to attach it all to the umbrella. The problem is that the whole circuit was one huge, heavy, awkward-to-handle octopus. The protective gel covering the LEDs loved to stick to everything it came in contact with — the table, other strips, the tags I’d attached to identify each strand, the umbrella ribs, etc. I would have liked to have 10 extra hands during install.

A much better approach would have been to build components, attach them to the umbrella, and then connect them together. Rather than solder the LEDs to the proto-board, a pair of JST 3-pin connectors with long leads would have done nicely. That way, I could have positioned the microcontroller on the umbrella, then positioned a strip on a rib, then connected the two.

This would make repairs easier also. Sadly, the solder on one of the strips failed during install and had to be repaired. It was next to impossible to bring a soldering iron up to the top of the umbrella. Luckily, I was able to take down the strip (it was no longer connected to the proto-board), solder some STRANDED wire (for increased flexibility), add heat-shrink to the pad (for greater strain relief), and clip on a few Wago connectors. Then I just re-attached the strip to the rib, and snappend the proto-board’s dangling wires into the Wagos. That’s the way I wish I’d done ALL the strips, but I didn’t have any Wagos and wasn’t sure if I could get them by the 4th.

The microcontroller and proto-board had their own installation challenges. I thought the pole and ribs were plastic. It turns out they’re metal. That’s a big short-circuit risk. I got around the problem by taping the proto-board to a piece of styrofoam to insulate it from the pole.

The placement of the microcontroller at the top of the pole makes programming changes an issue too! Here’s a picture of me having to put a music stand on top of the patio table in order to put the laptop within reach of the microcontroller USB cable. This is how I added additional effects post-install.

One final shortcoming of my install — you can’t close the umbrella in a storm. And I never animated the pole.

So…let’s just call this a prototype — better luck next holiday!

Enjoy the show!

P.S. The hot, humid weather of the 4th caused the adhesive on the back of the LED strips to fail. The strips started to hang straight down putting a lot of stress on solder connections. I ended up fixing this by using clear, packing tape. I’d lay a piece onto the LED side of the strip and then fold it around the edges until it grabbed the umbrella rib. So far, so good.

Color coded hex driver hack tool-tip

Organize your hex drivers using color

I use metric hex drivers to work on drones, 3D Printers, CNC machines, and a lot of other projects. My hex drivers have cylindrical handles with the metric size printed on one side so most of the time the sizes are not visible. Here are some random pictures of my desktop during projects with the drivers circled.

Drivers

My drivers are the common 1.5mm, 2.0mm, 2.5mm, and 3.0mm sizes typically used for M2, M2.5, and M3 hardware. You can see that even when the drivers are oriented correctly the straps in my RaceDayQuads drone field kit cover the dimensions.

Color Codes

I thought it would be great to color code them using the grooves in the handles. I already know the resistor color code standard and thought it would be natural to use the same colors to represent driver sizes.

I decided to use the color coding for the two digit size but skip the third exponent band as I don’t have 10mm+ drivers. I use a fixed “a.b” encoding where “a” is millimeters, and “b” is tenths of a millimeter. For example, the 2.5mm driver would have a red stripe for 2 followed by a green stripe for 5.

Colorful Rubber Bands

I was going to use enamel paint to manually paint the driver handles, but that would be arduous and potentially messy when it occurred to me I could use rubber bands. There are a lot of colorful pony tail and craft rubber band kits on the internet but it took a lot of searching to find a rubber band kit that had all of the resistor color code colors, which include a full rainbow plus black, grey, white, and brown. Then I found the Funtopia 1500 Pcs Small Elastic Hair Ties with Organizer Box Colorful for Girls, Kids, Hair Elastics for Thin or Thick Hair (24 Colors). For less than $10 I can label drivers for the rest of my natural life.

It did not disappoint.

Results

Now I can clearly see which driver to select and get the right one every time even without my glasses.
(The unlabeled driver in my RaceDayQuads kit is a Phillips screwdriver)

It’s not what I made, it’s how I made it — Scripting Inkscape

Over the last couple of years, my wife and I have accumulated several small ceramic tiles (and other similarly shaped items). Rather than lying them flat, it’s good to prop them up on a stand. I made a stand by laser cutting out pieces from 1/8″ wood scraps, glueing the pieces together and painting the result. Here’s a finished stand propping up a tile mounted to a cork backing.

The stand is a rectangular piece that’s the same size as (or a little smaller than) the tile. I call this the PLATE. I decided to tilt the plate 15 degrees off vertical. Glued to the back of the plate are two triangles that hold the plate at 15 deg. These are the LEGS. The legs have to be sized large enough that the plate can’t fall over backwards — because, center of gravity — but not so large as to extend above the plate.

The dimensions of the legs are easy to calculate with a little trigonometry (thank you,
Fr. Edwin Karlovich, C.R.).

The hypotenuse, h, of the triangle is the same length as the plate’s height. A 15 deg. angle from vertical means a 75 deg. angle from horizontal. The bottom back of the leg is 90 deg. We know 3 facts about the triangle – 1 length and 2 angles. That’s enough for us to determine the lengths of the remaining two sides. The width (or “base”) of the triangle is

and the height is

Simple calculations for just about any calculator.

The cut diagram shows how simple the geometry is – a rectangle for the plate, and a second rectangle split on a bias for the two legs. This is very easy to create in Inkscape.

But, I had several to do and more to do in the future. So I set about automating the process of drawing the parts.

Inkscape is extensible, and Scott Pakin wrote an extension called Simple Inkscape Scripting that allows you to write Python scripts to automate the drawing.

https://inkscape.org/~pakin/★simple-inkscape-scripting

After installing the extension, here’s the script I wrote to draw a cut-sheet for my stands. It’s a .py file created with a simple text editor. It exists as a file outside of Inkscape.

# Set the dimensions of the plate
plateWidth = 119
plateHeight = 97

# Do some trig to calculate the leg dimensions
angleDegrees = 75.0
angleRadians = angleDegrees * 0.01745329
legBase = cos(angleRadians) * plateHeight
legHeight = sin(angleRadians) * plateHeight
strokeWidth = 0.15


# Draw a rectangle
legs = rect((0, 0), (legBase, legHeight), stroke='#ff0000', stroke_width=strokeWidth)

# Draw a line from one corner of the rectangle to the opposite
diagonal = path(['m', 0,0, legBase, legHeight], stroke='#ff0000',
stroke_width=strokeWidth,
stroke_linecap='butt', stroke_linejoin='miter', stroke_opacity=1)

# Group the rectangle and line into a single object and move it off to the side
group([legs, diagonal], 'transform=translate(%.5f,%.5f)' % (plateWidth+10,0))

# Draw the plate rectangle
backplate = rect((0, 0), (plateWidth, plateHeight),
stroke='#ff0000', stroke_width=strokeWidth)

To use this script, edit the plate width and height and save the file. Inside of Inkscape, click Extensions, Render, Simple Inkscape Scripting… to open the dialog.

Browse to (or type) the name of the Python script file and click the APPLY button.

Now just send it off to the laser.

Rinse and repeat for any other stands with other dimensions.

The are several dozens of functions available within the scripting syntax to draw very complex images.

CTA Train Tracker – Part 2

“Sweet dreams and flying machines in pieces on the ground”

Taylor, J. (1970). Fire and Rain [Lyrics]

In my previous post related to this project (CTA Realtime Train Tracker), I explored the Chicago Transit Authority’s REST API from a software standpoint — what data is available, how do I access it, how can I display it. In this post, I present my efforts to build a illuminated map of the CTA rail system showing the actual locations of trains in real time.

Design Goals

Small Overall Dimensions and Light Weight17×22 inches wide at most
Geographically Suggestive👎🏻
👍🏻
Stop LabelsEach stop should be labeled with its official station name.
Bi-directional Traffic FlowThere should be something that shows the direction each train is moving.
Color CodedThe CTA Train Lines are called Red, Blue, Green, Orange, Pink, Purple, Yellow and Brown. The lights on a Line should reflect the Line’s name.
Minimal Light BleedThe glow from a light should only illuminate one stop.
Visible When Lights OffThe map needs to be printed onto the board so that it’s recognizable even if the lights are turned off.
Lights between StopsThere should be a light between each stop, to indicate that a train has left its previous station, but has not yet arrived at its next station stop.
Inline Loop
👎🏻
Easy Wiring
👎🏻

Construction

While researching this build, I came across a map of the CTA Rail System on the Chicago Tribune web site (pictured above in the Design Goals table). Every stop was labeled, the Loop was not in a separate call-out, and the placement of the Lines bears a strong suggestion of a geographically accurate map. The straight lines with minimal corners lend themselves to the use of NeoPixel strips. NeoPixel strips are easy to wire, easy to program and color coded.

NeoPixel strips come in several sizes. I found a set that included 150 pixels per meter – that’s 6.6mm spacing between them. A single strip that runs from Linden in Wilmette to 95th St in Chicago would require approximately 44 pixels for each named stop plus one pixel for the in-between stops. That translates into a map that’s 22 inches tall.

The strips are 4mm wide, which would also allow for two strips to be placed side-by-side. One strip for the “Southbound” trains and another for the “Northbound” trains. It would be clear which direction a train is moving by noting which strip its LED is on — just like cars on a road.

Multiple strips would be needed — roughly one for each of the eight Lines. Some stops are serviced by multiple Lines, so a given pixel might illuminate as red, brown, purple, etc. depending on which train was passing through. The strips can be cut, repositioned appropriately and rewired.

Programming

With an Adafruit RP2040 Scorpio, 8 LED strips can be driven from a single control board running CircuitPython. The Scorpio would require a Feather add-on to give it Wi-Fi capability.

The REST API has two key pieces of information to place a train — the station ID which the train is traveling towards and whether arrival at that station is imminent (<60 seconds out) or not. So, for each train running, look up the station ID in a table/dictionary. This table will contain the strip and pixel number for that station. If arrival is imminent, light that pixel with the color of the train. If arrival is not imminent, light the pixel one less than the number in the table.

This simple rule assumes the “Southbound” strip joins back up with the “Northbound” strip at the “southern” end forming one continuous strip of consecutively numbered pixels. The rule breaks down at the intersection of two (or more) Lines that share the same tracks. Luckily, the number of exceptions is manageable.

Display

With the electronics decided, the next decision is to address the look of the map. I envision a thin, wooden board that is laser etched with station names and with holes for the LEDs to shine through. The LED strips will be attached to the back of the board. The holes will keep the light focused. A layer of diffusing material could be placed to soften the lights. Colored lines can be painted on the board for each train Line to satisfy the “lights off” goal. An appropriate logo would be etched into the board as well.

Laser etching

The Space’s laser cutter is conveniently set up with Inkscape, which reads SVG files. SVG files are just plain text files formatted in XML. I could use Notepad to type an SVG file by hand and get that file cut on the laser.

My first experiment was to write a program to create an SVG to cut labels and holes on scrap matte board. I iterated over all the stations and used their lat/lons to place a hole and label it. For the labels, I went with a single-stroke font. This way, I could get a crisp, tiny vector etch of the labels rather than a smudgey raster burn. Inkscape 1.2 has an extension called Hershey Text which creates labels using Hershey Fonts. Hershey fonts are a collection of vector fonts developed c. 1967 by Dr. Allen Vincent Hershey at the Naval Weapons Laboratory, originally designed to be rendered using vectors on early cathode ray tube displays. While the Space’s laser cutter laptop doesn’t have an up-to-date copy of Inkscape, it will accept SVGs containing Hershey Fonts created elsewhere and render them on the laser.

Here is the results of my first test at creating an SVG programmatically.

As you can see, the Loop is hopelessly over etched and the station labels are very close together. Even if I used the largest board the laser’s bed could accommodate, I would not be able to eliminate the overcrowding.

Here’s a sample of Hershey Fonts on real wood. I ran the vector etch next to the raster etch to show the contrasting results of the two methods. I think vector wins hands down. The slight charring on the vector etch can be mediated with blue painter’s tape – although that would require a fair amount of weeding.

I’d convinced myself that I could, in fact, programmatically create an SVG file to draw the map. So I wrote a program that placed the stations of the Red Line in a straight line — spaced exactly to correspond to the distance between LEDs on a strip and I printed that SVG file on the laser.

Equally Spaced Stations

I attached an LED strip to the back of this board and animated it with some real-time train data. I only had one LED strip in inventory, so I illuminated southbound trains in Red and northbound trains in Blue. This Timelapse video shows the result.

The Hard Truth

It was at this point that I took stock of what I’d achieved and what remained to do. I realized that the project was going to be very expensive to complete. LED strips cost a lot and I’d need quite a few of them. Even with all my successes so far, the success of the final project was not assured. I was also disappointed that the partial prototype was a bit boring to watch in real-time. Only in time lapse does it approach being interesting. It just doesn’t seem worth it to proceed.

Alternative Designs

I could relax my design goals, e.g. one strip per Line instead of two and no in-between stops. But I think that will diminish the look of the project too much. Therefore, I’m moth-balling the project.

Still, I would like to see the entire rail network in operation. I looked for an alternative way to display the data. I settled on a small device called the PyPortal. I’d written about the PyPortal in this post, “The Adafruit PyPortal and Modifying Adafruit Libraries“. I ported my code to the PyPortal but rather than light up NeoPixels, I drew a colored dot on the screen to represent the position of each train.

Basic Program Flow

Here’s the guts of the program:

  1. Query the CTA for the latitude/longitude of all trains
  2. Map the lat/lon into screen coordinates
  3. Clear the screen
  4. Turn on the pixels at the screen coordinates
  5. Rinse, repeat

Step 3 caused an annoying flicker when the screen clears during each iteration.

I solved that problem by saving a list of all the pixels that were lit up. During the next iteration, I’d compare the list of prior screen coordinates with the list of new screen coordinates. If a pixel was in the prior list but is not in the current list — I’d turn the pixel off. Then I’d iterate through all the current pixels and turn them on. If the pixel is already on, it stays on — no harm no foul. If the pixel is new, it gets turned on. This is very fast and there is no flicker.

The Entire Network

Here is a time lapse of the PyPortal system running. The video is only 5 seconds, but may loop continuously.

The video is short for two reasons — 1) you get the point from the short video and 2) after just a few iterations, the program crashes. Other tests of my code running on macOS and a Raspberry Pi Zero did not abort. They run perfectly fine all through the night. After several weeks of very frustrating debugging, I’ve concluded there’s too much data for the PyPortal hardware and/or the CircuitPython libraries to process and the libraries do a poor job of handling the exceptions.

Stubborn Determination

I abandoned the PyPortal and made another attempt by attaching a tiny, TFT display onto a Raspberry Pi Zero.

(yeah, it really is that small.)

I rewrote the code (again) to draw onto that display. As I’d hoped, that program ran for many, many iterations without failing. The fact that the program runs continuously allowed me to experiment with tweaking the display.

One change I made was to the way I refreshed the screen at each iteration. Rather than clearing the pixels and drawing the updated positions, I just left all the prior iterations’ pixels turned on. After only a few iterations, the rail lines began to appear as thin lines. The end result is a glowy map of the entire CTA rail network — geographically accurate. (This is similar to what I did in Part 1 when I tracked individual trains and built a Google Earth map from the data.)

The downside of this is that once the lines are fully drawn, you can no longer dicern the current locations of the trains.

To fix that, I decided rather than turn pixels off or leave them on, I would instead turn the prior pixels dark grey. This made it easier to see both the current train locations as well as the line it was running on.

Taking it one step further, rather than grey for all lines, I set a train’s old pixel to a dimmed version of that color. Now all the trains (bright dots) are running on color coded lines (dim dots).

Finally, the TFT has two built-in buttons. I programmed them to toggle between two different versions of the data — one is the real-time train positions map, and the other is a bar chart showing how many trains are running on each line.

(Interesting aside: When photographed straight on, the camera sees the TFT colors as totally washed out and indistinguishable from one another. However, at a steep angle, the colors are saturated and vivid.)

End of the Line

While this TFT version of the map misses many of the design goals, I am at least able to see the entire CTA rail network in real time.

Many thanks to the members of Workshop 88 who shared their thoughts with me throughout the project. Without their involvement, this project might never have gotten as far as it did.

Final project

And with that… I’m done with this project (until I can find cheap, tightly spaced, LED strips).

CTA Realtime Train Tracker

I have spent many hours riding CTA elevated trains. Most notably from my home on the NW side of Chicago to Loyola University’s Downtown campus — Logan Square on the Blue Line to Washington, walk through the pedestrian tunnel to the Red Line, to Chicago Avenue. After graduation, I got an apartment in Evanston and took the Purple line from Linden in Wilmette to the Merchandise Mart where I worked.

So I took a great interest in an ad for this product:

It’s a map of the CTA lines with LEDs that light to indicate where the trains currently are — in real time. I wanted it. But at $300, it was out of reach. So, I’ll make one for myself.

The blog post is about my efforts to create a proof of concept in software before I consider a hardware solution.

The first step is to source the data. It turns out the CTA has a free, public REST API regarding where its trains are. There are multiple endpoints to the API. The most important to this project is ttpositions that shows the location of every active train on a particular line – Red, Blue, Orange, etc…

http://lapi.transitchicago.com/api/1.0/ttpositions.aspx?rt=red

and returns the following interesting values:

Train No.Next StopApproaching?LatitudeLongitudeHeading
811Chicago Ave.No41.903-87.63189
813WilsonYes41.964-41.964344

A little explanation will help here. Each active train has a unique identifying number. When a train reaches a terminus, the number is recycled.

Next stop shows the station name at which the train will make its next stop. Even if a train is sitting stopped at some station, it will be the next station down the line that will be returned. A train might also be between two stops.

If the train is just about to pull into a station, Approaching is “yes”.

Latitude and Longitude are the map coordinates of the train.

Heading is the compass direction for the train’s motion.

It’s simple to take these lat/lon data and create a KML (Keyhole Markup Language) file and feed that file into Google Earth Pro. KML files are XML. A KML file will contain XML elements to specify colors, font size, icon, and the position and orientation of the viewpoint — the point in space that determines what you’re looking at, etc.

The most critical element of the KML is a place marker for each train. The place marker looks like this:

<Placemark id="7">
  <name>605 Ridgeland 89°</name>
  <styleUrl>#g</styleUrl>
  <Point id="7">
    <coordinates>-87.79378,41.88699,300</coordinates>
  </Point>
</Placemark>

In this example, the train number, next station name and compass heading are used to label each dot on the map. styleUrl references a previously defined style that specifies the formatting of the place marker. The element coordinates specifies the lon, lat.

My minimally viable product runs a query for each CTA Line, outputs the data to the KML file, and opens the file in Google Earth.

If this is done repeatedly every 15, 30 or 60 seconds, you’ll be able to watch each train move through the system.

MACE, You’re Off the Rails!

A glaring hole in this version is the lack of context. We can’t see the elevated tracks.

The API can again help us with that. Another API endpoint is ttfollow which gives details about a specific train — including its coordinates

http://lapi.transitchicago.com/api/1.0/ttfollow.aspx?runnumber=811

My strategy to plot the train tracks was to find a train that was just leaving a terminus and repeatedly query that run number (train number) and collect lat/lon until that train reached the opposite terminus. That set of points can be coded into a KML file as a PolyLine drawn from point to point.

Sounds simple, but that’s when my frustrations began to grow.

Challenges

Zeroth, The documentation for the API says that when you query a line for all trains, it returns an array of train dictionaries. But if there is only one active train on the line, rather than a one element array, it’s a scalar dictionary. And if no trains are running on the line, the “trains” JSON key doesn’t even exist. This wasn’t a problem earlier until I did the Purple line during non-rush hour and the Yellow (Skokie Swift) line. I found solutions, but it makes the code kludgey.

First, it’s difficult to catch a train at a terminus. It’s not like the train is sitting at the O’Hare terminal, broadcasting its GPS and a station name of O’Hare. The best I could do was randomly catch a train soon after it left a terminus moving toward its next stop – Rosemont. So I had to frequently query the Blue Line hoping to see a Rosemont train HEADING EAST (as opposed to a Rosement train heading west from Cumberland!). Once I saw a train starting a run, I had to take its number and train queries.

OK — annoying, but then, patience is a virtue.

Second, as I was monitoring the progress of a train it got to somewhere near the Belmont station when POOF — It drops off the radar never to return. I have no idea why that train disappeared and I have seen this anomaly multiple times since.

OK — annoying, but…

Third, after I’d collected about 60 data points, I plotted them and saw a most bizarre path.

The train appeared to jump forward then back then forward again. I observed this behavior many times and I didn’t believe this is what the train was actually doing. I concluded there is something wrong with the data being returned from the API. Letters to the API manager have gone unanswered.

I ended up developing a workflow where I could identify these spacetime anomalies and correct them. If you look at the illustration above, if you get rid of point #3 (which is the exact same coordinates of point #7), the data looks realistic and reasonable. I imported the data points into Excel and used conditional formatting to highlight duplicates:

Then I simply deleted the first duplicate (which unformats that dup’s twin) and then moved on to the next “first” duplicate. Once no duplicates are present, I put the datapoints into the KML file to plot the line.

I went through this process for each of the CTA Lines.

The end result of all this is the following graphic:

When the train positions are overlayed onto the colored Lines, you get:

If you repeatedly plot the trains, you get a timelapse of the movement of the trains through the system.

https://youtu.be/C2WNu9fGgoc

This exercise shows the hardware version of this project is entirely possible. A prototype of the Purple Line seems very doable. It will need 9 LEDs for each stop plus 8 more for between-station lights. I want a string of LEDs for Southbound trains and a second string for Northbound trains. That makes 36 LEDs total. The processing flow would be:

Query the Purple line and get its trains  
For each train in AllTrains
   Look at the Next Stop and direction of each train
   If Approaching is Yes, 
      light up the station
   Else
   If Approaching is No, 
      light the between-station-LED just prior to Next Stop
Rinse, Repeat

There is still the issue of time warping, but I no longer care. I’ll just assume that if a train is reported at a given lon/lat, then that’s where it is.

Next Stop

The next step is to build a prototype using LED lights and migrate the software to a Raspberry Pi.

Using ChatGPT to Help Locate My Birthstar

I’m 68.56 years old today. My birthstar is that star which is 68.56 light-years away from Earth. The light produced by that star on my birth date is just reaching Earth now! Later, when I’m older, my birthstar will be different.

I wanted to write a program that would list my birthstar(s). I asked my astrophysist friend where I could find an easily queried, public access, database that contained stellar distances. She suggested the Gaia Archive. https://gea.esac.esa.int/archive/

I soon learned that the Gaia Archive contains a table called gaiadr3.gaia_source. This table contains data such as

FieldMeaning
source_id Unique ID assigned to this star by the Gaia project
raBarycentric right ascension 𝛼 of the source in ICRS at the reference epoch ref_epoch (degrees)
decBarycentric declination 𝛿 of the source in ICRS at the reference epoch ref_epoch (degrees)
ipd_gof_harmonic_phaseThe phase of the variation of the IPD GoF (reduced chi-square) as function of the position angle of the scan direction. (degrees) Clear as mud.

I knew that right ascension and declination are important for locating stellar objects. (Who can forget the scene in Contact when Jodie Foster first hears alien signals from space and relays the coordinates of those signals to her team in the control room of the VLA radio telescope. https://youtu.be/5WSqMGJpAZk)

But I couldn’t find anything in the table called distance.

ChatGPT

ChatGPT is an artificial intelligence project that has made a lot of news recently. ChatGPT is currently available for free at https://chat.openai.com. I thought I’d use it to help me figure out if the Gaia Archive can tell me my birthstar.

When you launch a ChatGPT session, called a chat, you start with a simple text box. I started my chat with

me: how can I use the Gaia archive to give me a list of stars that are a specific distance away

(The responses from ChatGPT can be verbose, so I’ll redact some of them.)

ChatGPT: …you can perform a query using the Gaia archive’s query interface. The query would likely involve specifying a distance range and using the parallax measurement to calculate the distance to each star. …you can use “ADQL” (Archive Data Query Language) which is similar to SQL…

That sounds promising. I know SQL and I can use parallax to get the distance.

me: how do you compute distance from parallax

ChatGPT: Distance (in parsecs) = 1 / (parallax angle in arcseconds)

That’s a pretty easy formula. I’ll be able to program that into other applications related to astronomy. Now to get a list of stars and their parallax values.

me: write a Gaia archive adql query to list stars that are 69 light years away

I kid you not. I asked ChatGPT to write an ADQL query that I could just paste into the web page that ChatGPT referred to in the first answer. And ChatGPT did it.

ChatGPT: Here is an example of an ADQL query that can be used to list stars that are 69 light years away using the Gaia archive:

SELECT 
    source_id, ra, dec, parallax
FROM 
    gaia_dr3.gaia_source
WHERE 
    parallax >= (3.08567758e-05 - 0.00005) AND
    parallax <= (3.08567758e-05 + 0.00005)

I pasted the code into Gaia’s query interface and got syntax errors. What followed was some back and forth with ChatGPT regarding its bogus advice.

me: that query has unresolved identifiers
it: I apologize for any confusion, you are correct that the query I provided in my previous response would not work as written…
me: the query you gave me should use gaiadr3.gaia_source as the from parameter
it: I apologize for the mistake in my previous response

Once ChatGPT’s syntax errors were corrected, the query ran for 30 minutes and returned more than 3,000,000 stars that are (supposedly) between 60 and 70 light years away. I kept narrowing down the range and eventually got the results down to 37,000 stars. I don’t know if that’s the correct number so I tried to determine the parallax measurement for a 69 light-year distant star using paper and pencil.

It turns out that while parallax is normally measured in arcseconds, Gaia records milli-arcseconds. ChatGPT didn’t seem to know that. I eventually came up with the correct values to plug into a Gaia query.

REST API

The queries up to this point were very web-interactive with a lot of keyboard and mouse clicks. I wanted to use a command line tool. So I asked,

me: does the Gaia archive have a REST API

ChatGPT: Gaia archive does have a REST API… Currently, the main endpoint of the Gaia archive REST API is: https://gea.esac.esa.int/tap-server/tap

Reading the REST documentation, there’s a Python module that can be installed to make queries easy. I installed it and asked

me: write a python to query the Gaia archive that lists ra, dec and parallax for stars that are 69 light years distant and use the astroquery.gaia module

ChatGPT:

from astroquery.gaia import Gaia

# Define the query
query = """
SELECT ra, dec, parallax
FROM gaiadr2.gaia_source
WHERE 1 / parallax * 0.001 * 3.26 = 69
"""

# Execute the query
job = Gaia.launch_job_async(query)
result = job.get_results()

# Print the result
print(result)

AND IT WORKS! I threw some bells and whistles into the Python and can now get anyone’s birthstar by supplying a birthdate on the command line.

> p3 birthstar.py -h
birthstar.py [-b:][-n][-h]
-b birthdate yyyymmdd  default: 19540627
-n Northern Hemisphere default: False
-h print usage

> p3 birthstar.py -b 19570411 -n
The person was born 4/11/1957
They are 65.77 years old today
Looking for stars 65.76986301369857 light-years distant (+/- 7 light-days)
Target parallax is 49.5906
Only stars in the Northern hemisphere will be listed.
Here's the Gaia ADQL query

SELECT
    source_id, ra, dec, parallax
FROM 
    gaiadr3.gaia_source_lite
WHERE 
    parallax <= 49.60507969737007 AND 
    parallax >= 49.5761590662218 
    AND dec > 0
ORDER BY parallax DESC

Output saved to: 1674002905756O-result.csv

source_id	        ra         dec	   parallax
1858219151114880000  312.54398   29.38393  49.59437
3270079526697710000   56.77948    1.64475  49.58366
3914019231742220000  170.93122    8.56434  49.58089

Conclusion

ChatGPT was an absolute wonder to work with. It understood what I wanted. And despite the misinformation it gave me and its use of magic numbers in its code, I don’t think I could have finished this project as quickly as I did without consulting it.

The Adafruit PyPortal and Modifying Adafruit Libraries

Adafruit.com has a large selection of electronic components to buy. They also have a very active YouTube channel, a large number of Learning Guides, and software libraries to control all that hardware. Adafruit’s PyPortal dev board is a wi-fi enabled device with a small, color, touch-enabled screen. The PyPortal is programmed using CircuitPython and Adafruit has published several learning guides for it.

Example projects

Here are just a few of the PyPortal projects that I’ve downloaded and modified:


  1. Event Countdown Timer
    Modification: A vacation countdown timer that cycles through graphics representing my next four vacation destinations and the number of days until that vacation begins.

  2. ISS Tracker
    Modification: Lengthened the satellite’s trail to show the entire orbit just completed.

  3. Hurricane Tracker
    Modification: Added trails to show where the hurricanes has been, not just where they are now.

  4. Cleveland Museum of Art Display Frame
    Modification: wait for it…

These applications all have one thing in common. They all reach out to the Internet to grab data and then display that data on the screen. When multiple applications follow the same general pattern, then a lot of effort can be saved by using a well written software library — and Adafruit has written one for the PyPortal.

Example code

To get a feel for the PyPortal’s power and ease-of-use, let’s look at the Quote of the Day project. When you point your browser to https://www.adafruit.com/api/quotes.php, you’ll get back something like this:

[
 {
 "text": "Somewhere, something incredible is waiting to be known.", 
 "author": "Sharon Begley",
 }
]

This JSON response from the web site has two pieces of text that we’d like to display on the screen – the quote and the author’s name. To do that using the Adafruit PyPortal Library, the program must create a PyPortal object and initialize it with all the relevant information:

portal = PyPortal(
         url='https://www.adafruit.com/api/quotes.php',
         json_path=([0, "text"], 
                    [0, "author"]),
         text_position=((20, 120),  # screen location for quote 
                        ( 5, 210)), # screen location for author 
         text_color=(0xFFFFFF,      # quote text color 
                     0x8080FF),     # author text color 
         text_wrap=(35,             # characters to wrap for quote 
                     0),            # no wrap for author 
         text_maxlen=(180,          # max length for quote
                       30),         # max length for author
         text_font="/fonts/Arial-ItalicMT-17.bdf",
  )
while True:
     portal.fetch()
     time.sleep(60)

The loop refreshes the screen with a new quote every 60 seconds using the PyPortal’s fetch() method.

The PyPortal library does all the heavy lifting of connecting to your LAN, browsing to the site’s url, converting the returned JSON into a Python dictionary, and displaying the text on screen. Loads of applications follow this same fetch/display model — current weather data, current number of “likes” on my latest YouTube post(s), latest bitcoin price, local gas prices, current DuPage County covid case count, etc.

Displaying Images

The Cleveland Museum of Art Display Frame Learning Guide follows the same general pattern, but includes the the download and display of an image as well as text. (All the previous examples use a static background image.). Let’s see how PyPortal library handles dynamic images. Here’s the new code:

pyportal = PyPortal(
       json_path=["data", 0, "title"],
       text_position=(4, 231),
       text_color=0xFFFFFF,
       text_font="/fonts/OpenSans-9.bdf",                    
       
       image_json_path=["data", 0, "images", "web", "url"],
       image_dim_json_path=(["data",0,"images","web","width"],
                            ["data",0,"images","web","height"])
       image_resize=(320, 225),
       image_position=(0, 0),
 )

The text parameters are still there. So what’s different? For one thing, there is no URL. We’ll deal with that later. And there are new parameters to deal with the image:

  • image_json_path – points to the URL of the downloadable image
  • image_dim_json_path – points to the dimensions of the downloadable image
  • image_resize – fit the image into these dimensions
  • image_position – indicates where on the screen to place the resized image

Now, what about the URL for the Museum? In the Quote project, the URL never changed. Each time you .fetch()’d that URL, a different quote was returned. But for the Museum project, you have to provide a different URL for each piece of art in the Museum’s collection. There are 31954 pieces in the Museum collection. The URL for the 54th piece would include an &skip=53 parameter as part of the URL. The main loop below will do that — starting with the 1st piece and sequentially displaying all the pieces.

skipcount=0
while True:
   pyportal.fetch(f'http://.....&skip={skipcount}')
   skipcount+=1
   time.sleep(60)

Art Institute of Chicago (ARTIC)

Having recently visited the Art Institute of Chicago, I got to thinking — can I modify the CMA project to display the art from the ARTIC. Does the ARTIC even have a website like the CMA? YES it does – https://www.artic.edu/open-access/public-api! So I started coding up the PyPortal parameters and ran into a BIG roadblock. The ARTIC database does not list the full path to a piece of art’s downloadable image. The URL must be constructed from several bits of data. Here’s a highly redacted JSON response for a specific piece of ARTIC art:

{
  "data": [
    {      
      "thumbnail": {
          "width": 3000,
          "height": 1502
      },
      "image_id": "cb34b0a8-bc51-d063-aab1-47c7debf3a7b",
      "title": "Ballet at the Paris Opéra"
    }
  ],
  "config": {
    "iiif_url": "https://www.artic.edu/iiif/2",
  }
}

The title of the artwork is straightforward.

The image’s URL can be computed with the following code:

[“config”,”iiif_url”]+”/”+[“data”,0,”image_id’]+”/full/!320,240/0/default.jpg”

I.e.,

https://www.artic.edu/iiif/2/cb34b0a8-bc51-d063-aab1-47c7debf3a7b/full/!320,240/0/default.jpg

but that doesn’t fit the PyPortal library’s assumption that the JSON will contain the downloadable image’s URL in a single key:value pair.

Open Source

Then at 3:00 AM, I had the idea of editing the PyPortal Library Open Source. So I downloaded the adafruit_ciruitpython_pyportal/adafruit_pyportal library from Github and started searching through the code. I found the right spot in the fetch() method, inserted a patch to build the image url and add that URL back into the JSON using a new key named ‘artic_image_URL’. The PyPortal has no idea that the image_url key is something I created and slipped in rather than coming from the Museum.

I also had to add code to stuff the image’s dimensions into the JSON. I started with [“thumbnail”][“width”] and [“thumbnail”][“height”] from the ARTIC JSON. These are the dimensions of the master, thumbnail image. But I requested !320,240 in the image_url which means the ARTIC would send me an image that fits within a 320×240 bounding box. The actual, downloaded image size might be 166×240 or 320×185 — to preserve the aspect ratio. So I had to compute the dimensions that the PyPortal library was actually going to encounter.

Here’s the complete patch that I made to the Adafruit library:

            # Build the image URL
            PREFIX = json_out["config"]["iiif_url"] + "/"
            IMAGE = json_out["data"][0]["image_id"]
            OPTIONS = '/full/!320,240/0/default.jpg'
            json_out["artic_image_URL"] = PREFIX + IMAGE + OPTIONS
            
            # Compute the aspect of the Library's artwork
            image_Width = json_out['data'][0]['thumbnail']['width']
            image_Height = json_out['data'][0]['thumbnail']['height']
            aspect = image_Width / image_Height

            # Resize the artwork to fit in a 320 x 240 frame
            if aspect > 320.0/240.0:
                 thumb_width = 320
                 thumb_height = int(320 * image_Height / image_Width)
            elif aspect < 320.0/240.0:
                 thumb_height = 240
                 thumb_width = int(240 * image_Width / image_Height)
            else:
                 thumb_width = 320
                 thumb_height = 240
            json_out["thumb_width"] = thumb_width
            json_out["thumb_height"] =  thumb_height

And here is the PyPortal code that retrieves ARTIC images:

portal = PyPortal(image_json_path=["artic_image_URL"],
                  image_dim_json_path=(["thumbnail_width"],
                                       ["thumbnail_height"]),
                 ...)

To get the PyPortal device to run my patched code instead of the original, “compiled” library code, I simply had to replace the /lib/adafruit_circuitpython_pyportal/__init__.mpy file with my modified __init__.py. My .py file happily coexists along side of all the “compiled byte code” .mpy library files.

And this is what appears on the PyPortal:

ARTIC Splash Screen
Ballet at the Paris Opera
Yellow Dancers (In the Wings)

etc.

Downsides

My solution isn’t flawless. For some reason, landscape oriented thumbnails are not getting resized correctly, but portrait oriented thumbnails are. Secondly, my patch isn’t robust. If I try to run the Hurricane Tracker project and my patch is in play, then the app will crash. I’ve created a Github issue asking that a JSON transform function be created to allow pre-processing of the JSON. I’m waiting to see if anyone takes an interest. https://github.com/adafruit/Adafruit_CircuitPython_PyPortal/issues/126 if you want to “Like and Subscribe” the issue.