How do I selectively edit hex bytes in a byte array without changing other

Question:

I’m writing a python program that is sending a byte array to a BLE device using a byte array. I can manipulate the data sent to the device and that works. The device is an 8 button Bluetooth switch panel. The byte array when read from the device looks like this

x08x00x00x00x00

To clarify what this means in context, x08 I believe is the header which is position 0 of the array. Position 1 is switch 1 and 2, Position 2 is switch 3 and 4, and so on

The array appears to be looking at each position as a hex code so if I want to turn on switch number 4 I have to send 01 but if I want to turn on switch 3 I have to send 16 if I want both it would be 17 which would breakdown something like this

Switch 4 only
switch_status[2] = 01

output:
x08x00x01x00x00

Switch 3 only
x08x00x16x00x00

There are other functions like momentary mode and flash but I’m keeping this simple for now.

I’m trying to avoid writing an entire function that writes the entire array each time as I don’t want to manipulate all the switch just to turn one switch on. However since each byte in the array controls 2 switches I either have to run a check to determine the state then change the byte for both switches to ensure I’m not turning on or off the wrong switch or I need to find a better way to manipulate the array elements. I’m not sure what the right call is here. Any advice would be appreciated. I’m still learning python so I may be using the wrong concepts here.

I’ve attempted directly writing the bytes for individual switch pairs and that works. However it makes the code a bit cumbersome when I have to check each pair for current status and write if statements to ensure I’m not overwriting a value I want to keep if another switch is already on. I can do that but it seems inefficient. Could be that is the correct answer but I thought I would ask. I’ve considered dumping the array to a string and converting back after a specific edit has been made but that seemed like over handling. If statements by comparison seemed more efficient. Then end goal is to turn this into a function call that gets used by another part of the app. The ability to reliably push commands over BLE to do what I need the switches to do was the logical first step.

Here is the code I’m working on

import asyncio
from bleak import BleakClient

#Define the device we are manipulating

address = "{DEVICE HW ADDRESS}"
SWITCH_UUID = "0000fff1-0000-1000-8000-00805f9b34fb"

#Define our Variables. Outside of testing these should all be set to 0
sign = 0
left = 0
right =
rear = 0
bar = 0
cargo = 0
jump = 0
booster = 0

#Convert our variables to a hex string then store them in a byte array. At some point we will need to get current switch status so we can parse and change variables in the array to reflect current configuration.
simplearray = "08" + str(sign) + str(left) + str(right) + str(rear) + str(bar) + str(cargo) + str(jump) + str(booster)
#array = bytes.fromhex("08" + str(sign) + str(left) + str(right) + str(rear) + str(bar) + str(cargo) + str(jump) + str(booster))
array = bytes.fromhex(simplearray)

#Print list of current states for switches for troubleshooting.
print("Switch1: " + simplearray[2])
print("Switch2: " + simplearray[3])
print("Switch3: " + simplearray[4])
print("Switch4: " + simplearray[5])
print("Switch5: " + simplearray[6])
print("Switch6: " + simplearray[7])
print("Switch7: " + simplearray[8])
print("Switch8: " + simplearray[9])

#Use asyncio to connect to our BLE GATT server
async def main(address):
    async with BleakClient(address) as client:

        #verify connection to our server
        print(f"Connected: {client.is_connected}")

        #Get the switch status from the UUID imput above and print current status. Later we will use this to make sure we are only changing desired switches.
        switch_status = await client.read_gatt_char(SWITCH_UUID)
        print("Switch Status:" + str(switch_status))

        #Set security level. What is this for anyway?
        paired = await client.pair(protection_level=2)

        #Make sure we are paired before operating switches. Error handler needed here.
        print(f"Paired: {paired}")

        #Signal lights to turn on and re-read status.
        print("Turning Switches On....")

        #For some reason this doesn't work. Can't manually assign values to a position in simplearray.
        #Would have to use a replace command instead but leaves us sending the entire hex byte array to the unit instead of switching one output like we want
        #will likely read hex value and manipulate for values we want
        #simplearray[5] = 1
        #print(simplearray[5])

        #print output of the array to check our status while testing. Will be removed later
        print(array)
        #This sends the array to the switch. I can send commands to individual pairs but not individual switches. Will need to investigate further.
        await client.write_gatt_char(SWITCH_UUID, array)
        switch_status = await client.read_gatt_char(SWITCH_UUID)
        print(switch_status)
        await asyncio.sleep(5.0)
        print(switch_status[0])
        print(switch_status[1])
        print(switch_status[2])
        print(switch_status[3])
        print(switch_status[4])
        #Signla turn switches off after wait.
        print("Turning Switches Off...")

        #For testing purposes turn all switches off before we exit.
        await client.write_gatt_char(SWITCH_UUID, b"x08x00x00x00x00")
        switch_status = await client.read_gatt_char(SWITCH_UUID)
        print(switch_status)
        await asyncio.sleep(1.0)

#Disconnect I guess? Look into this one.
asyncio.run(main(address))
Asked By: CommGuy

||

Answers:

Half a byte is often called a nibble. Using Python bitwise operations to modify the nibble for the switch you want to turn on/off is maybe the way to go.

I created two functions called swtich_on and switch_off. They take the current value from the SWITCH_UUID read after it is converted to an integer. Using int.from_bytes to do this.

To get the value to write to the SWITCH_UUID characteristic, int.to_bytes is used.

Here is a test I did to turn all the switches on then off again using these two functions:

def switch_on(switch_status: int, switch_id: int) -> int:
    if switch_id % 2:
        switch_id += 2
    switch_status |= 1 << (4 * switch_id)
    return switch_status


def switch_off(switch_status: int, switch_id: int) -> int:
    if switch_id % 2:
        switch_id += 2
    switch_mask = 1 << (4 * switch_id) ^ 0xFFFFFFFFFF
    return switch_status & switch_mask


def main():
    switch_int = int.from_bytes(b'x08x00x00x00x00', 'little')  # current value to integer
    print(f"[start]  {switch_int.to_bytes(5, 'little')} ({switch_int})n")
    for switch_id in range(1, 9):
        switch_int = switch_on(switch_int, switch_id)
        print(
            f"[ON]  ID={switch_id} : {switch_int.to_bytes(5, 'little')} ({switch_int:})"
        )
        switch_int = switch_off(switch_int, switch_id)
        print(
            f"[OFF] ID={switch_id} : {switch_int.to_bytes(5, 'little')} ({switch_int:})n"
        )


if __name__ == "__main__":
    main()

which gave a transcript of:

[start]  b'x08x00x00x00x00' (8)

[ON]  ID=1 : b'x08x10x00x00x00' (4104)
[OFF] ID=1 : b'x08x00x00x00x00' (8)

[ON]  ID=2 : b'x08x01x00x00x00' (264)
[OFF] ID=2 : b'x08x00x00x00x00' (8)

[ON]  ID=3 : b'x08x00x10x00x00' (1048584)
[OFF] ID=3 : b'x08x00x00x00x00' (8)

[ON]  ID=4 : b'x08x00x01x00x00' (65544)
[OFF] ID=4 : b'x08x00x00x00x00' (8)

[ON]  ID=5 : b'x08x00x00x10x00' (268435464)
[OFF] ID=5 : b'x08x00x00x00x00' (8)

[ON]  ID=6 : b'x08x00x00x01x00' (16777224)
[OFF] ID=6 : b'x08x00x00x00x00' (8)

[ON]  ID=7 : b'x08x00x00x00x10' (68719476744)
[OFF] ID=7 : b'x08x00x00x00x00' (8)

[ON]  ID=8 : b'x08x00x00x00x01' (4294967304)
[OFF] ID=8 : b'x08x00x00x00x00' (8)

If you start with all the switches on, the the switch_on function has no effect because that switch is on already and the switches get turned off one by one with the switch_off function.

[start]  b'x08x11x11x11x11' (73300775176)

[ON]  ID=1 : b'x08x11x11x11x11' (73300775176)
[OFF] ID=1 : b'x08x01x11x11x11' (73300771080)

[ON]  ID=2 : b'x08x01x11x11x11' (73300771080)
[OFF] ID=2 : b'x08x00x11x11x11' (73300770824)

[ON]  ID=3 : b'x08x00x11x11x11' (73300770824)
[OFF] ID=3 : b'x08x00x01x11x11' (73299722248)

[ON]  ID=4 : b'x08x00x01x11x11' (73299722248)
[OFF] ID=4 : b'x08x00x00x11x11' (73299656712)

[ON]  ID=5 : b'x08x00x00x11x11' (73299656712)
[OFF] ID=5 : b'x08x00x00x01x11' (73031221256)

[ON]  ID=6 : b'x08x00x00x01x11' (73031221256)
[OFF] ID=6 : b'x08x00x00x00x11' (73014444040)

[ON]  ID=7 : b'x08x00x00x00x11' (73014444040)
[OFF] ID=7 : b'x08x00x00x00x01' (4294967304)

[ON]  ID=8 : b'x08x00x00x00x01' (4294967304)
[OFF] ID=8 : b'x08x00x00x00x00' (8)
Answered By: ukBaz

Ok thanks to @ukBaz I found a solution. It wasn’t what I was hoping for but I’m constrained by the fact that I have to send a command to at least 2 switches. working with bytearray directly may have been a fruitful endeavor but I think this solution will fit my needs better. The benefit I see is that the gui I end up building for this will be able to get status from the bluetooth device in realtime and a simple change of state will be easier and require less code using this as a base. I can also use it to automate certain tasks in other areas as well.

Here is the working code if anyone is interested.

And again, thank you @ukBaz, I’m grateful you took the time.

import asyncio
from bleak import BleakClient

#Define the device we are manipulating

address = "(MAC ADDRESS TO SWITCH)"
SWITCH_UUID = "CHARACTERISTIC UUID"


def switch_on(switch_status: int, switch_id: int) -> int:
    if switch_id % 2:
        switch_id += 2
    switch_status |= 1 << (4 * switch_id)
    return switch_status

def switch_off(switch_status: int, switch_id: int) -> int:
    if switch_id % 2:
        switch_id += 2
    switch_mask = 1 << (4 * switch_id) ^ 0xFFFFFFFFFF
    return switch_status & switch_mask


#Use asyncio to connect to our BLE GATT server
async def main(address):
   async with BleakClient(address) as client:
     switch_status = await client.read_gatt_char(SWITCH_UUID)
     switch_int = int.from_bytes(switch_status, 'little')  # current value to integer
     print(f"[start]  {switch_int.to_bytes(5, 'little')} ({switch_int})n")

     print(f"Connected: {client.is_connected}")
     paired = await client.pair(protection_level=2)
     print(f"Paired: {paired}")

     print("Turning Switches On....")

     print("Which switch to operate: 1 - 8")

     switch_id = int(input())

     print("On or Off?")
     state = input()

     if state == 'on':
          state_change = switch_on
     elif state == 'off':
          state_change = switch_off
     print("Switch " + str(switch_id) + " Selected")

     switch_int = state_change(switch_int, switch_id)
     operate = switch_int.to_bytes(5, 'little')
     await client.write_gatt_char(SWITCH_UUID, operate)

     print(
         f"[ON]  ID={switch_id} : {switch_int.to_bytes(5, 'little')} ({switch_int:})"
     )

#Disconnect I guess? Look into this one.
asyncio.run(main(address))
Answered By: CommGuy