Toggle Switches Make a Macro Keyboard with psBASIC
The 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 IF
s. 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.