Toggle Switches Make a Macro Keyboard with psBASIC

Written by ChipMaster on 2024-11-05 14:50:58 updated 2024-11-07 14:53:47

The WallCHIPv2 Control Panel

WallCHIPv2 Control Panel

The Project

In the past two episodes we were able to grab system statistics and reflect them on to a set of 8 LEDs. The last part of this particular project is to use the 8 toggle switches for something. In essence they will make a macro keyboard where each switch will run some piece of software based on its new state. This gives us 16 possible actions.

This is a bit of a nebulous topic since what you want to do with 8 switches or digital inputs is likely to be different then what I've done with them. The desired actions are going to be dependent on you and what situation you put your computer into. I used the switches to turn on and off audio playback, select audio sources, trigger additional lighting systems and control the output of the 8 LEDs. The LEDs can reflect two different stats, the switch states (for testing) or be in an all-off state, in case the blinking becomes a nuisance.

To recap, just a bit, the control panel depicted above is controlled by a single ATMega328, which is connected to a Raspberry Pi 2B via an I2C bus. A single byte write to the I2C slave turns on and off the LEDs, one per bit. While a read operation returns the 8 switches encoded into a single byte, one per bit. Basically the ATMega328 is an 8 in and 8 out GPIO expander.

So I guess the goals of this particular exorcise are simply going to be to illustrate reading the 8bit byte from the I2C device and provide some ways in which the states of the switches can trigger 16 possible actions. Most likely the actual action decoding code would be different depending on what you want to accomplish.

The I2C read

      SW=0 : ' Current switch states
      SWP=0 : ' Previous switch states - to catch all ons when started
  120 open "B",#1,"/dev/i2c-1:17" ' Door controller
      '...
10000 SWP=SW : ' Save previous switch state
      s_$=" "
      get #1,,s_$
      SW=asc(s_$)
      return

Line 120 open's I2C bus #1 to talk to slave ID 17. This is the same as in the previous example. The BINARY file mode is a read and write mode. The GET command on a BINARY file reads a variable's width worth of bytes into it. Since we initialized the string with a single byte we're reading a single byte with this GET command. We convert the incoming byte into an integer with the asc() function. Now we're free to do something with the value.

Decoding switches into actions.

      gosub 10000 : ' Read switches into SW
      bit = 1
      for x=0 to 7
         c = SW and bit
         if c<>(SWP and bit) then
            ' ON ... GOSUB starts at 1 for the first line number
            on x*2+1+abs(c<>0) gosub 30000, 30100, 30200, 30300, 30400, 30500, 30600, 30700, 30800, 30900, 31000, 31100, 31200, 31300, 31400, 31500 
         end if
         bit=bit*2
      next

This bit of code walks through the 8 bits retrieved from the I2C bus, each being a switch. If the switch position has changed then the ON ... GOSUB ... line is used to execute one of 16 possible subroutines. The first subroutine at line 30000 is called when switch 0 is moved to the off position. 30100 is called when switch 0 is turned on. 30200 is called when switch 1 is turned off, and so on. Since the values returned from comparison operators are either 0 or -1 I used abs() to change this to 0 or 1. This is added to the switch number times 2 to give the progression described.

The ON ... GOSUB could be replaced with a SELECT CASE statement and long IFs. It could be written like this:

      gosub 10000 : ' Read switches into SW
      bit = 1
      for x=0 to 7
         c = SW and bit
         if c<>(SWP and bit) then
            select case x
               case 0
                  if c then
                     '... switch on action
                  else
                     '... switch off action
                  end if
               case 1
                  if c then
                     '...
                  else
                     '...
                  end if
               case 2

               '... and so on to case 7

            end select
         end if
         bit=bit*2
      next

I personally prefer the first version because its keeps the algorithms of each part of the code separate. But I realize its considered bad style to use line numbers these days. But in the first example the decode logic is distinct from whatever actions are performed and IMO more obvious as to what its doing, since its not cluttered with the action code. And then the action code is distinct and not cluttered with the decode logic.

The ON ... GOSUB is also faster since its a simple indexed branch where the SELECT CASE ... is a stack of tests. Each condition is tried in turn, starting from the top and working down until a condition matches. This means that actions at the top execute faster than actions at the bottom.

One could keep the SELECT CASE uncluttered with the use of SUB and FUNCTION. But that adds another set of jumps. Still its your choice.

What about switches tied direct to GPIO inputs?

The simplest way to wire up switches is with a pull-up resistor on one side and ground on the other. This inverts the action of the switch where the switch being off produces a high input (1) and being on produces a low (0). Since its likely that switches would be tied to an odd arrangement of GPIOs lets use a "map" array, like this:

      data 10,14,13,16,2,15,18,19
      dim map%(8), SWP%(8), SW%(8)
      mat read map%
      mat SW%=con : ' Fills with 1s (offs) - to catch all ons when started
      ' ...
      for x=1 to 8
         SWP%(x)=SW%(x)
         SW%(x) = inp(map%(x))
         if SW%(x)<>SWP%(x) then
            ' ON ... GOSUB starts at 1 for the first line number
            on x*2-SW%(x) gosub 30000, 30100, 30200, 30300, 30400, 30500, 30600, 30700, 30800, 30900, 31000, 31100, 31200, 31300, 31400, 31500 
         end if
      next
      ' ...

The formula in the ON ... GOSUB may not be obvious. My math leverages the inverted input state and inverts the operations. The counter variable (x) is being run from 1 to 8, not 0 to 7 as in the previous example. This was because the MAT ... series commands always start at array element 1 and ignore 0. Since the first switch is now 1, not 0 as in the previous example, x*2 is now 2 as opposed to 0. If the switch is physically off inp() returns 1, which when subtracted from 2 is 1, which calls subroutine 30000. When the switch is on inp() returns 0, which when subtracted from 2 is 2, calling subroutine 30100. This means that for this discussion the subroutine numbers are identical regardless of which input method is used.

NOTE: The use of MAT READ and MAT CON were simply typing savers. They could have easily been done with a FOR loop. So this code could have been implemented with indexes 0-7 as opposed to 1-8. And, of course, the actual switch read could have also been implemented with a subroutine at line 10000. Your choices.

What to do with the switches?

At this point the only real question left is what to do with the switches. I think of these switches as a "macro keyboard" since each switch action can issue a command, not just send a single key stroke. Two things that I do with my switches is to play music and turn on and off decorative lighting. So let me fill in some example actions for controlling the "XMMS2" music player and toggling a GPIO bit to control a relay. I'll base this example on the ON ... GOSUB variation of code above.

      ' Switch #1 off - turn off music
30000 shell("xmms2 stop")
      return
      ' Switch #1 on - turn on music
30100 shell("xmms2 play")
      return
      ' Switch #2 off - playlist 1
30200 shell("xmms2 playlist switch mix1")
      return
      ' Switch #2 on - playlist 2
30300 shell("xmms2 playlist switch mix2")
      return
      ' Switch #3 off - turn off ambient light control relay on GPIO 1
30400 out 1,0
      return
      ' Switch #3 on - turn on ambient light control relay on GPIO 1
30500 out 1,1
      return

This is a simplistic example. XMMS2 tends to be a bit unruly. So you'd likely want to manage some state information to issue a "play" command if it stops and the switch is still in the on position. When switching play lists it may be important to issue a "stop", followed by a "play" command if the play switch is in the on position. You might want to use more than one switch to select playlists. It might also be desirable to read a "daylight" sensor and turn on and off the light control relay appropriately.

And there are many more things one could do that have nothing to do with music or lighting.

Putting it all together

      '*** GLOBALS ***

  100 defint A-Z
      dim field$(20)
      delimit #10," "+string$(2,0)+"1" : ' Parse variable space separated ASCII tables
      BUSYCT=0 : ' The value shown on the door LEDs
      BUSYCTP=255 : ' Previous BUSYCT
      SW=0 : ' Current switch states
      SWP=0 : ' Previous switch states
      open "B",#1,"/dev/i2c-1:17" : ' Open Door controller: I2C1 ID 11H

      '*** Output loop ***

  200 gosub 10000 : ' Read switches into SW
      bit = 1
      for x=0 to 7
         c = SW and bit
         if c<>(SWP and bit) then
            on x*2+abs(c<>0) gosub 30000,30100,30200,30300,30400,30500,30600,30700,30800,30900,31000,31100,31200,31300,31400,31500
         end if
         bit=bit*2
      next
      gosub 20100 : ' CPU time
      if BUSYCT<>BUSYCTP then gosub 10100 : ' Write door LEDs
      'REST A BIT AND THEN DO IT AGAIN
      sleep 0.05 : ' We don't need to drive the CPU at 100% load
      goto 200
      ' We should never exit the loop

      '*** Door Read ***

10000 SWP=SW : ' Save previous switch state
      s_$=" "
      get #1,,s_$
      SW=asc(s_$)
      return

      '*** Door Write ***

10100 s_$=chr$(BUSYCT) : ' Pack a 1 byte write
      put #1,,s_$
      BUSYCTP=BUSYCT
      return

      '*** CPU Work load ***

20100 BUSYCT=0
      open "I",#10,"/proc/stat"
      mat input #10,field$
      close #10
      BUSYCT=(VAL(field$(2))+val(field$(4))) and 255
      return

      '*** Switch Actions ***

      ' Switch #1 off - turn off music
30000 shell("xmms2 stop")
      return
      ' Switch #1 on - turn on music
30100 shell("xmms2 play")
      return
      ' Switch #2 off - playlist 1
30200 shell("xmms2 playlist switch mix1")
      return
      ' Switch #2 on - playlist 2
30300 shell("xmms2 playlist switch mix2")
      return
      ' Switch #3 off - turn off ambient light control relay on GPIO 1
30400 out 1,0
      return
      ' Switch #3 on - turn on ambient light control relay on GPIO 1
30500 out 1,1
      return
      ' Switch #4 off
30600 return
      ' Switch #4 on
30700 return
      ' Switch #5 off
30800 return
      ' Switch #5 on
30900 return
      ' Switch #6 off
31000 return
      ' Switch #6 on
31100 return
      ' Switch #7 off
31200 return
      ' Switch #7 on
31300 return
      ' Switch #8 off
31400 return
      ' Switch #8 on
31500 return

NOTE: All of the line numbers used in an ON ... GOTO or ON ... GOSUB command must exist or the code will produce an error before it can be run. So I filled them in with do nothing subroutines. One could also delete or remark the excess line numbers out until you were ready to do something with them.

Get your copy of Pi Shack BASIC here!