please choosego to mobile | Continue to access the PC version
View: 7685|Reply: 7

Infrared proxy for aircooling system without LIRC

[Copy link]

7

threads

38

posts

181

credits

Registered member

Rank: 2

credits
181
Published in 2018-2-18 04:52:42 | Show all floors |Read mode
Edited by DoubleHP at 2018-2-19 06:06

The final aim of the project is to develop a multifunction proxy for an air cooling system.

I want to be able to control the indoor units (I may call them terminals) via network, shut them down, change temperature, or be able to perform any operation remotely.

Because this installation is in an hostel, I also want to restrict the usage of users. Customers of the hostel are allowed to switch the system on and off, and choose temperature, but I want to limit some aspects to avoid various issues.

The master unit is
AOY30LMAW4

The detailed schematics are here:
http://www.airclim.com.ua/userfi ... BAJ_AOY_LMAW4sm.pdf

***

Users are allowed to manipulate the infra red controller.

The first problem I had was that some users have set the units to a too high value, and this may over consume electricity. To avoid this, I need to analyse the IR frames sent from the remote, to the unit, and do some tricks. Then, when I see a user asked for 26°C, I send a new frame with temperature set to 24°C.

The second issue is that the master unit manages 4 indoor terminals; when one is set as heater, and an other one as cooler, then, the master unit goes in "locked mode", and does nothing at all, but just blinks everywhere. To prevent this, I will force all terminals to always work in automatic mode.

The third issue is that I want an anti-freeze mode. The system does not allow any temperature below 18°C. I do not want to heat the whole house at 18°C when it's empty during weeks long; during winter, I only want to heat it at +6°C. And if indoor tempearture is between +6 and +18, I want to keep the whole system shutdown.

I also wanted to try to make the pi invisible; and avoid using a true IR emeter.

*** Hardware part, air cooling system.

The indoor units are very similar to the ones shown on page 9 of the above PDF.

Why do I have great hope things can be done when looking at this schematic ? I see several power supplies, connectors, and wires. The unit is not one big PCB, but there are wires around, and I can easily access them.

Points of interessed are:
- on top right side, in the indicator PCB, the SBX1810 is an infra red receiver. The out pin is the line 201-7, which becomes line 10-8, which goes to pin 63 after a weak pullup resistor. This part is designed the classic way; data line has a pullup, logic is inverted, what means, the line usually sleeps at +5V; and is shortcircuited to 0V to transmit things. I will call this line IR-OUT, or IR-DATA. In practice, this line is a long wire, about 60cm long running in the terminal; it's very easy to either cut the line and insert a derivation, or use a vampire connector.
- in the same region of the scematic, we easily find a "local ground", the 0V level.
- below the indicator PCB, we find a TEST plug, which includes an other ground pin. This pin has a better gauge than the ground wire of the INDICATOR, and is better suited for ground connections, and supply.
- The indicator PCB include a +5V line, but it comes from a 7805 which are usually rated for 1A or 3A. The Orange pi consumes between 0.2 and 0.3A; and I do not want to load the built-in/manufacturer's 7805 with this if it's designed for 1A. So I am notgoing to supply my opi from here; but on the DIFFUSER plug, there is a 14V pin. Again, I can easily catch this pin, and put a vampire connection on the wire. Conversion from 14V to 5V is done with a "car conversion module 12V-5V micro USB" https://www.amazon.fr/gp/product ... 3_s00?ie=UTF8&psc=1 ... or this model because I prefer the 90° plug (which turns in the good direction for opi) https://www.amazon.fr/Transforma ... Z9VVCEPHQV45B5CMP16 .

Please, pay attention to the fact the "local ground" (aka 0V, the small ground with vertical lines below) is NOT connected to "earth" (yellow wire coming with the live mains, phase and neutral, which uses a triple horizontal lines symbol). This detail is important. And I will do my best to use these words correctly: ground vs earth.

By plugging the pi directly to the IR-DATA line, I can at the same time:
- receive IR frames; the pi will receive exactly the same IR data as the main board of the terminal, disregarding any light condition, reflection, or else. I do catch exactly the same data as the terminal.
- because the IR receiver is iddleing at high state, and pulling the data line down to send data, it's trivial for the pi to also send data on this line, by grabbing the line down when required, and leaving the line floating otherwise.

*** Orange pi startings

I chose the Orange Pi Zero cheapest version 256M, H2+. For SDcard, I had many problems with cheap cards, so let's use good quality ones: Sandisk Ultra white-grey 16GB; a smaller card would be cheaper, but I could not find them anymore. The supply is mentionned above.

I will also use a DS18B20 1W (1wire) temperature probe. They are sold with 1m wires for 2€ on Amazon.

The disk image I used is Armbian_5.35_Orangepizero_Ubuntu_xenial_next_4.13.16.img
https://www.armbian.com/orange-pi-zero/
Note that the version 5.35 will change after update (after running aptitude update / aptitude distupgrade, the version will be updated to 5.38 or more ...).

Also, it's critical to know that this image is using a 4.13.16 Linux kernel. There have been huge changes between line 3 and line 4 kernels, and all other tutorials that were written before mid 2017 are deprecated and unusable on Linux 4.* . The explanations below do not apply to Linux 3.* .

Most people write the disk image using dd; I use a wrapper that does many tricks for me; I will not cover this part here. This tutorial is not about step-by-step complete setup; I will only cover what's specific to my project. But, I let you know that my wrapper does many things: get rid of root password change and user creation, setup fixed IPv4 and IPv6, and setup my VPN; and install many packages (make, unison, gcc, git ...). Before the very first time I log in the pi, all this is already done. I won't either talk about SSH. My wrapper sets-up RSA auth keys, so that I don't have to think about passwords at all.

*** Reading temperature.

This is probably the easiest part.

Edit /boot/armbianEnv.txt and on the line overlays, add the w1-gpio word; and param_w1_pin=PA15 on the next one; it will look like this:

  1. # cat /boot/armbianEnv.txt
  2. verbosity=1
  3. logo=disabled
  4. console=both
  5. disp_mode=1920x1080p60
  6. overlay_prefix=sun8i-h3
  7. overlays=cir usbhost2 usbhost3 w1-gpio
  8. param_w1_pin=PA15
  9. rootdev=UUID=ebe9dacf-124f-486c-b6c1-08749e209374
  10. rootfstype=ext4
  11. usbstoragequirks=0x2537:0x1066:u,0x2537:0x1068:u
Copy code

Note that this setup is specific to the Orange Pi Zero H2+ (common for 256 and 512), due to the fact we have an additionnal memory chip on the first SPI bus.

On RAspberry Pi forums, I have learnt that 1w requires time critical commands, and that only the SPI bus is able to provide this; so, in short, 1W can be handled only on SPI pins. The oPi has two SPI bus; the first one is dedicated to the memory chip at the back (16MB IIRC), a small bsquare chip with 8 legs on the back of the pi, just in the middle, you can't miss it. Recent opi0 have a larger memory; opi that are not in the 0 range do not have it (and they have only one SPI bus). All this explains why the w1-gpio wrapper can not use the default first bus, and needs to be instructed to use the second SPI bus, the one on PA15. This may vary on other opi0 (the ones which are not H2+ or H3: H5 and other bananas). And it will differ on non 0 pis.

After this modification, you need to reboot. I give all commands in one shot:

  1. # ls /sys/bus/w1/devices/
  2. 28-041703225eff  w1_bus_master1
  3. # cat /sys/bus/w1/devices/28*/w1_slave
  4. 4f 01 4b 46 7f ff 0c 10 46 : crc=46 YES
  5. 4f 01 4b 46 7f ff 0c 10 46 t=20937
Copy code

And from an other pi which has two probes:

  1. # ls /sys/bus/w1/devices/
  2. 28-0517023468ff  28-0517025727ff  w1_bus_master1
  3. # cat /sys/bus/w1/devices/28*/w1_slave
  4. 8f 00 4b 46 7f ff 0c 10 a0 : crc=a0 YES
  5. 8f 00 4b 46 7f ff 0c 10 a0 t=8937
  6. 92 00 4b 46 7f ff 0c 10 88 : crc=88 YES
  7. 92 00 4b 46 7f ff 0c 10 88 t=9125
Copy code
A quick conversion into °C :

  1. # cat /sys/bus/w1/devices/28*/w1_slave | grep "t=" | cut -d "=" -f2 | awk '{print $1/1000}'
  2. 8.937
  3. 9.125
Copy code

Note that if you have other non DS18B20 1W devices, this quick script will break.

I also remind you that things like 28-0517023468ff  28-0517025727ff are world uniq 64b identifiers. You can rely on them; they can not be predicted when you buy a new item. Once you installed a new item on a pi, this identifier will never change.

*** My failed attemps to use lirc-alsa

https://forum.armbian.com/topic/ ... s-on-orangepi-zero/

In that thread, I explain that I have found a bug in mode2, how to use (in theory) an Alsa sound card to read and emit IR frames, and how it all failed for me.

7

threads

38

posts

181

credits

Registered member

Rank: 2

credits
181
 Author| Published in 2018-2-18 04:53:20 | Show all floors
Edited by DoubleHP at 2018-2-18 23:38

*** IR digression

A classic introduction to the basic NEC protocol; this is how most TV IR remotes work. Every one messing with IR must know this base; but, with my Fujitsu air cooler, things are going to be completely different:
https://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol
The main idea you have to remember is:
  • A "logical 0" is a 562.5 microsecond pulse, followed by a        562.5 microsecond gap.
  • A "logical 1" is a 562.5 microsecond pulse, followed by a        1687.5 microsecond gap.
except that for Fujitsu, the basic timing will be 400us instead of 562.

These topics deals about LIRC to read IR; but they are using very old Armbian, not using DTO/DTB, and do not work any more with recent Armbian:
https://forum.armbian.com/topic/ ... receive-irinfrared/
http://codelectron.com/how-to-setup-infrared-remote-control-in-orange-pi-zero-using-lircd-and-python/

Also, whatever you read about RAspberry Pi, they are very lucky people, because they have a working trivial driver called:
  1. irc_dev
  2. lirc_rpi gpio_in_pin=23 gpio_out_pin=22
Copy code

This is magically simple; but I did not find any equivalent for Orange/Armbian.

I believe that to transmit IR frames, it should be possible to edit the "cir" overlay, and ask him to send frames on a GPIO. But I did not take time to look for the source of the cir overlay.

This guy is doing wonderfull work with pigpio; not usable on orange pi:
http://blog.bschwind.com/2016/05 ... ry-pi-without-lirc/

A very good introduction to Infra-Red:
http://www.instructables.com/id/ ... nditioning-control/
not usable for me; his frames are completely different from Fujitsu. In particular, the checksum works a different way. Panasonic is doing things the traditionnal way (without initial constant, and without decrementing the byte count).

If you configure your pi (or buy an FTDI USB dongle) to set a full serial port, maybe you can ask LIRC to use GPIOs. It is probably possible to ask the chipset to configure a complete UART2, and then, you can use the following tutorial to ask LIRC to use it to transmit data:
https://pmgration.wordpress.com/ ... and-receivers-ftdi/
But this tutorial is not clear about which pins of the FTDI are used for reception and emission; so I did not want to loose time on this, and started writing my own wrapers.

*** Reading infra red

First, add "cir" to the overlays line, and reboot (see above). Now you have a new device node /dev/lirc0 . Install lirc:

  1. aptitude install lirc
Copy code

As explained in the above link, I lost days on bugs; long story short, I use mode2 to get the raw timings, and wrote a bash script to convert them into bit-stream. This code produces one line on stdout per frame; garbage is sent on stderr. The line is sent in one shot; I used to output bits one by one, but happened to be uncompatible with other features of my project; so the bit's are kept in cache untill end of frame, or timeout.

  1. # cat read_ir.sh
  2. #!/bin/bash

  3. n=1
  4. t=1
  5. v=400
  6. mode2 -d /dev/lirc0 | {
  7.         frame=""
  8. while true ; do
  9.         read -t 1 line
  10.         [ "$line" = "" ] && {
  11.                 # timeout
  12.                 [ $n -eq 0 ] && {
  13.                         echo "$frame"
  14.                         frame=""
  15.                         n=1
  16.                 }
  17.                 t=1
  18.                 continue
  19.         }
  20. #echo "_ $line _" >&2
  21.         n=0
  22.         # the awk methods works, but creation of awk process takes too much time
  23.         # bash internals are faster
  24. #       typ="$(echo "$line" | awk '{print $1}')"
  25. #       val="$(echo "$line" | awk '{print $2}')"
  26.         array=( $line )
  27.         typ=${array[0]}
  28.         val=${array[1]}
  29.         z=0
  30. #       [ $val -gt $(($v/2)) ] && [ $val -lt $(($v*2)) ] && r=s && z=1
  31. #       [ $val -gt $(($v*2)) ] && [ $val -lt $(($v*4)) ] && r=m && z=1
  32. #       [ $val -gt $(($v*8)) ] && [ $val -lt $(($v*12)) ] && r=l && z=1
  33.         y=$((($val+$v/2)/$v))
  34.         [ $y -eq 1 ] && r=s && z=1
  35.         [ $y -eq 3 ] && r=m && z=1
  36.         [ $y -eq 4 ] && r=l && z=1
  37.         [ $y -eq 7 -o $y -eq 8 -o $y -eq 9 ] && r=x && z=1
  38.         [ $y -gt 20 ] && r=xx && z=1
  39.         [ $z -ne 1 ] && {
  40.                 echo ".${val}/$y ???" >&2
  41.                 continue
  42.         }
  43. #       echo "$typ $r" >&2
  44.         [ "$typ" = "space" ] && {
  45.                 case "$r" in
  46.                         "s")
  47.                                 frame="${frame}0"
  48.                                 t=0
  49.                                 ;;
  50.                         "m")
  51.                                 frame="${frame}1"
  52.                                 t=0
  53.                                 ;;
  54.                         "l")
  55.                                 [ $pp = "x" ] && {
  56.                                         # begin of frame
  57.                                         frame="${frame}S"
  58.                                 } || {
  59.                                         # error
  60.                                         echo "LARGE" >&2
  61.                                 }
  62.                                 ;;
  63.                         "x")
  64.                                 true
  65.                                 echo "???X" >&2
  66.                                 ;;
  67.                         "xx")
  68.                                 true
  69.                                 echo "???Y" >&2
  70.                                 ;;
  71.                         *)
  72.                                 echo "???T" >&2
  73.                 esac
  74.                 pp=0
  75.                 continue
  76.         }
  77.         # here, we have a pulse.
  78.         case "$r" in
  79.                 "s")
  80.                         true
  81.                         ;;
  82.                 "l")
  83.                         true
  84.                         ;;
  85.                 "x")
  86.                         # begin of frame ?
  87.                         [ $t -eq 0 ] && echo "$frame"
  88.                         [ $t -eq 0 ] && frame=""
  89.                         t=1
  90.                         ;;
  91.                 *)
  92.                         echo "???P" >&2
  93.         esac
  94.         pp=$r   # previous pulse
  95. done
  96. }
Copy code

Do not forget chmod +x ...

This code is completely specific to my project. It will work only with Fujitsu remotes for air cooling systems. I have found some explanations about the frame details in here:
http://www.hifi-remote.com/johnsfine/DecodeIR.html
In very short: the base timing is t=400us. The start sequence is a 8t pulse followed by 4t space. At electronic level, an infra red pulse becomes a 0V shortcut; and a space (absence of light) is when the receiver leaves the IR-DATA lines floating, what means +5V due to the pullup. Then, '0' bit is 1t pulse then 1t space; '1' bit is 1t pulse then 3t space. There is a "stop condition", made of a 1t pulse, followed with a 110t space. The stop condition is always eaten away by LIRC; but it's required to know it exists, when we want to retransmit frames.

7

threads

38

posts

181

credits

Registered member

Rank: 2

credits
181
 Author| Published in 2018-2-18 04:53:58 | Show all floors
Edited by DoubleHP at 2018-2-18 22:53

*** reverse engineering frames, and producing new frames (the "proxy" part).

Here comes the heavy job.

All frames are not born equal.

There are short and long frames.

Short frames are usually the "turn off" instruction; there is an other case, I forgot if it's "dry coil", or "fan blow".

All frames start with the same sequence. The Hifi-Remote link gives clues about it; the two first bytes (16 bits) are manufacturer identifier ("20:8,99:8,0:4" means decimal '20' writen over 8 bits, decimal '99' written over 8 bits, decimal '0' written over 4 bits - did not understood what E-D-S-F means).

The last byte is a checksum; took me days to understand how it works. You initiate a variable at '111' (decimal), add all received data (all but the last one) bytes in this variable, substract the number of received data bytes (7 for short frames, 15 for long frames). To this result, you apply a modulo 256 (this is equivalent to performading AND 0x11111111), and substract it from 255 (equivalent to XOR 0x11111111).

Note that in the frame, bytes are send as little endian (least significat bit first). So string manipulation is required to reverse this into big endian, which is the expected format for BASH. And of course, the result must again be reverted before being compared to actual received checksum, or sending it.

My code computes two checksums at the same time. Variable CRC6 contains the checksum for the received frame; CRC8 is for transmitted frame (if we need to transmit).

As explained above, this code does many things in one shot:
- I max the temperature at 24°C
- I force the mode to AUTO, and prevent from using heat, cool, or dry mode. I only allow auto and fan mode.

I believe that the protocol could accept termperatures from 16 to 31; I did not try to forge such commands; did not have the patience to wait 3 days and see if the room temp matches the forged command.

The logic is simple:
- in all case, we build the frame to be sent
- if any change is done to any byte, I set the 'resend' variable
- if this variable is set, then, we send the frame

There is a timeout at 5s; every 5s, we check for cache files; if they exist, we send the command. Commands are pre-set frames. This is designed to avoid conflits on emission PGIO pin. This script is the only one which may call transmission program.

Any transmission waits 1s to avoid conflicts with the remote.

I do not detect if the transmitted frame is sent correctly. I could: if next received frame is not identical to last sent frame, then, re-emit it.

To transmit an order, just touch a file matching variable name in /dev/shm .

The following comments have been removed from the next post in order to make the code fit the 10000 char limit:

  1. # Sending the OFF frame at start has several purposes:
  2. # - put the terminal in a known state at boot time
  3. # - switch things off by default; reboots are rare; people can turn it on later if they need.
  4. # - work around the LIRC bug that drpops the last bit of first frame; so that the next signal send by user can be received correctly (and won't fall in the "invalid too short frame" detector).

  5. # bash decimal to binary converter
  6. # https://stackoverflow.com/questions/10278513/bash-shell-decimal-to-binary-conversion

  7. # TODO: reset last_sent to "" after next frame is received

  8. # https://stackoverflow.com/questions/11461625/reverse-the-order-of-characters-in-a-string

  9. # http://www.hifi-remote.com/johnsfine/DecodeIR.html
  10. # https://electronics.stackexchange.com/questions/183785/understanding-ir-protocol-checksum-generation/356781#356781

  11. #[ "${D2B[${crcl6}]}" = "${byte}" ] && echo "CRC6 is good" || echo "CRC6 is bad :/ $(((2#${byte}-$crcl6+256)%256))"
  12. #[ "${D2B[${crcl8}]}" = "${byte}" ] && echo "CRC8 is good" || echo "CRC8 is bad :/ $(((2#${byte}-$crcl8+256)%256))"
Copy code

7

threads

38

posts

181

credits

Registered member

Rank: 2

credits
181
 Author| Published in 2018-2-18 18:51:55 | Show all floors
Edited by DoubleHP at 2018-2-18 21:27

.

  1. # cat write_ir.sh
  2. #!/bin/bash

  3. # OFF:
  4. frame_off="S00101000110001100000000000001000000010000100000010111111"

  5. # turn on at +18 @auto
  6. frame_on_auto="S00101000110001100000000000001000000010000111111110010000000011001000010000000000000000000000000000000000000000000000010011110001"

  7. # turn on at +18 @heat
  8. frame_on_heat="S00101000110001100000000000001000000010000111111110010000000011001000010000100000000000000000000000000000000000000000010011010001"
  9. last_sent=""

  10. frame_header="0010100011000110000000000000100000001000"

  11. rm /dev/shm/frame_o*
  12. touch /dev/shm/frame_off

  13. send_frame() {

  14.                 echo "S: $lino" >&2
  15.                 str=""
  16.                 while read -N 1 char
  17.                 do
  18.                         [ "$char" = "" ] && break
  19.                         [ "$char" = "S" ] && {
  20.                                 str="$l"
  21.                                 str="${str}\n$(($s+$m))"
  22.                                 continue
  23.                         }
  24.                         str="${str}\n$s"
  25.                         [ $char -eq 0 ] && str="${str}\n$s" || str="${str}\n$m"
  26.                 done <<<"$(echo "$lino")"
  27.                 # STOP signal
  28.                 str="${str}\n$s"
  29.                 str="${str}\n$(($s*110))"
  30.                 str="${str}\n987654321" # my EOF to kill transmission

  31.                 last_sent="${lino}"
  32.                 sleep 1
  33.                 echo -e ${str} | tcc -lwiringPi -run /root/transmit_ir.c

  34. }

  35. d=-16
  36. s=400
  37. m=$(($s*3+$d))
  38. l=$(($s*8+$d))
  39. s=$(($s+$d))

  40. D2B=({0..1}{0..1}{0..1}{0..1}{0..1}{0..1}{0..1}{0..1})

  41. while true
  42. do
  43.         read -t 5 line
  44.         [ "$line" = "" ] && {
  45.                 # empty frame or timeout
  46.                 for i in frame_off frame_on_auto frame_on_heat
  47.                 do
  48.                 [ -e "/dev/shm/$i" ] && {
  49.                         rm "/dev/shm/$i"
  50.                         lino="${!i}"
  51.                         send_frame
  52.                 }
  53.                 done
  54.                 continue
  55.         }
  56.         echo
  57.         echo "R: $line" >&2

  58.         [ $((${#line})) -lt $((7*8+1)) ] && {
  59.                 # a frame must be a least 7 bytes = 42 bits, plus the start 'S'
  60.                 echo "Ignoring short/bad frame."
  61.                 continue
  62.         }
  63.         [ $((${#line})) -eq $((7*8+1)) -o $((${#line})) -eq $((16*8+1)) ] || {
  64.                 # a frame must be a least 7 bytes = 42 bits, plus the start 'S'
  65.                
  66. echo "Ignoring frame: BAD LENGTH: $((${#line})) is neigther $((7*8+1))
  67. or $((16*8+1)) (including start bit 'S')."
  68.                 continue
  69.         }

  70.         [ "$last_sent" != "" ] && {
  71.                 lastfs="${last_sent:0:$((${#last_sent}-8))}"
  72.                 linens="${line:0:$((${#line}-8))}"
  73.                 [ "${lastfs}" = "${linens}" ] && {
  74.                         echo "Skipping work on frame we just sent." >&2
  75.                         continue
  76.                 }
  77.         }

  78.       
  79. [  ${#line} -gt $((${#frame_header}+1)) ] && [
  80. "S${frame_header}" != "${line:0:$((${#frame_header}+1))}" ] && {
  81.                 echo "Frame header is not designed for us; skipping." >&2
  82.                 continue
  83.         }

  84.         lino=""
  85.         ct=0
  86.         byte=""
  87.         byto=""
  88.         byte_flushed=1
  89.         bct=0
  90.         g_on=x
  91.         g_ton=x
  92.         g_off=x
  93.         fan_blow=x
  94.         fan_mode=x
  95.         fan_swing=x
  96.         temp=x
  97.         coil_dry=x
  98.         sleep_s=x
  99.         master=x
  100.         crc6=$((255-142-2))     # compute it as decimal
  101.         crc8=$((111))   # compute it as decimal
  102.         crct6=0
  103.         crct8=0
  104.         resend=0

  105.         while read -N 1 char
  106.         do
  107.                 [ "$char" = "" ] && break
  108.                 [ "$char" = "S" ] && {
  109.                         [ $ct -eq 0 ] && {
  110.                                 lino="S"
  111.                                 byte_flushed=1
  112.                                 continue
  113.                         } || {
  114.                                 echo "Start bit 'S'was found in middle of a line. This is invalid. Skipping." >&2
  115.                                 lino=""
  116.                                 break
  117.                         }
  118.                 }
  119.                 ct=$(($ct+1))
  120.                 [ $(($ct%8)) -eq 1 ] && bct=$(($bct+1))
  121.                 [ $(($ct%8)) -eq 1 -a $ct -ne 1 ] && {
  122.                         docrc=0
  123.                         [ $bct -gt 0 ] && docrc=1
  124.                         [ $docrc -eq 1 ] && crc6=$((${crc6}+(2#${byte})))
  125.                         [ $docrc -eq 1 ] && [ $bct -ne 16 ] && crc6=$((${crc6}-1))
  126.                         [ $docrc -eq 1 ] && crct6=$(($crct6+1))
  127.                 #       echo -n " post6. #${byte}b ($((2#${byte}))).$bct. ${crc6} .${crct6}.${docrc}." >&2
  128.                 }
  129.                 [ $(($ct%8)) -eq 1 -a $ct -ne 1 ] && {
  130.                         docrc=0
  131.                         [ $bct -gt 0 ] && docrc=1
  132.                         [ $docrc -eq 1 ] && crc8=$((${crc8}+(2#${byto})))
  133.                         [ $docrc -eq 1 ] && [ $bct -ne 16 ] && crc8=$((${crc8}-1))
  134.                         [ $docrc -eq 1 ] && crct8=$(($crct8+1))
  135.                 #       echo " post8. #${byto}b .$bct. ${crc8} .${crct8}.${docrc}." >&2
  136.                 }
  137.                 [ $(($ct%8)) -eq 1 ] && {
  138.                         a="$byto"
  139.                         len=${#a}
  140.                         for ((i=0;i<len;i++)); do a=${a:i*2:1}$a; done; a=${a:0:len}
  141.                         lino="${lino}${a}"
  142.                         byte_flushed=1
  143.                         byte=""
  144.                         byto=""
  145.                 }
  146.                 byte="${char}${byte}"   # reverse order of bits in byte
  147.                 byto="${char}${byto}"   # reverse order of bits in byte
  148.                 byte_flushed=0
  149.                 #echo "$ct: $char" >&2
  150.                 [ $ct -eq 48 ] && {
  151.                         [ "$byte" = "00000010" ] && g_off=1 && g_on=0
  152.                         [ "$byte" = "11111110" ] && g_on=1
  153.                         [ "$byte" = "01101100" ] && fan_blow=1 && g_on=X
  154.                 }
  155.                 [ $ct -eq 72 ] && {
  156.                         temp=${byto:0:4}
  157.                         [ $g_on -eq 1 ] 2>/dev/null && {
  158.                                 temp_dec="$((2#${temp}))"       # binary to decimal
  159.                                 temp_deco=$temp_dec
  160.                                 [ $temp_dec -gt 8 ] && temp_dec=8 && resend=1
  161.                                 temp8b=${D2B[${temp_dec}]} # decimal to binray
  162.                                 temp4b=${temp8b:4:4}
  163.                                 lowbyto=${byto:4:4}
  164.                                 byto="${temp4b}${lowbyto}"
  165.                         }
  166.                 }
  167.                 [ $ct -eq 80 ] && {
  168.                         #master=${byto:5:3}
  169.                         master=${byto:4:4}
  170.                         mastero="$master"
  171.                         # only allow auto or fan
  172.                         #[ "$master" = "000" -o "$master" = "011" ] || {
  173.                         #       master="000"
  174.                         #       resend=1
  175.                         [ "$master" = "0000" -o "$master" = "0110" ] || {
  176.                                 master="0000"
  177.                                 resend=1
  178.                         }
  179.                         lowbyto=${byto:0:4}
  180.                         byto="${master}${lowbyto}"
  181.                 }
  182.                 [ $ct -eq 65 ] && g_ton=$char
  183.                 [ $ct -eq 76 ] && coil_dry=$char
  184.                 [ $ct -eq 85 ] && fan_swing=$char
  185.                         # force it to AUTO: 0000xxxx
  186.                         # coil dry: 00010xxx
  187.                         # sleep: xxx01xxx
  188.                         # leave it untouched
  189.                 echo -n "$char" >&2
  190.                 [ $(($ct%8)) -eq 0 ] && echo -n "." >&2
  191.         done <<<"$(echo "$line")"
  192.         echo >&2
  193.         crc6=$(($crc6%256))
  194.         crc8=$(($crc8%256))
  195.         crcl6=$((255-($crc6%256)))
  196.         crcl8=$((255-($crc8%256)))

  197.       
  198. [ "${temp}" = "x" ] && temp_dec=0 && tempa=0 &&
  199. tempo=0 && otemp=0 && tempstr="x" ||
  200. tempstr="${temp}/$((${temp_deco}+16))>$((${temp_dec}+16))/${temp4b}"
  201.         [ "${master}" = "000" ] && master="auto"
  202.         [ "${master}" = "001" ] && master="cool"
  203.         [ "${master}" = "010" ] && master="dry"
  204.         [ "${master}" = "011" ] && master="fan"
  205.         [ "${master}" = "100" ] && master="heat"
  206.         [ "${mastero}" = "000" ] && mastero="auto"
  207.         [ "${mastero}" = "001" ] && mastero="cool"
  208.         [ "${mastero}" = "010" ] && mastero="dry"
  209.         [ "${mastero}" = "011" ] && mastero="fan"
  210.         [ "${mastero}" = "100" ] && mastero="heat"
  211.       
  212. echo "Off(${g_off}) On(${g_on}) Ot(${g_ton}) T(${tempstr})
  213. M(${mastero}>${master}) Dry(${coil_dry}) Blow(${fan_blow})
  214. swing($fan_swing) CheckRecomp(${crc6}::${D2B[${crcl6}]}:${crct6})
  215. CheckOutput(${crc8}::${D2B[${crcl8}]}:${crct8}) CheckActual(${byte})"
  216. >&2
  217.         #echo >&2
  218.         [ "$g_off" = "1" -o "$g_on" = "1" -o "${fan_blow}" = "1" ] || {
  219.                 echo "Invalid frame; ignoring it ..."
  220.                 continue
  221.         }

  222.         [ "${g_off}" = "1" ] && echo "Off short frame. Skipping." && continue
  223.         [ "${fan_blow}" = "1" ] && echo "Blow short frame. Skipping." && continue

  224.         [ $resend -eq 1 ] && {
  225.                 [ $byte_flushed -ne 1 ] 2>/dev/null && {
  226.                         a="${D2B[${crcl8}]}"
  227.                         len=${#a}
  228.                         # https://stackoverflow.com/questions/11461625/reverse-the-order-of-characters-in-a-string
  229.                         for ((i=0;i<len;i++)); do a=${a:i*2:1}$a; done; a=${a:0:len}
  230.                         lino="${lino}${a}"
  231.                 }

  232.                 send_frame
  233.                 true
  234.         } || echo "Frame was fine."
  235.         sleep 1
  236. done
Copy code

7

threads

38

posts

181

credits

Registered member

Rank: 2

credits
181
 Author| Published in 2018-2-18 18:52:13 | Show all floors
Edited by DoubleHP at 2018-2-18 21:28

*** IR transmission code

Because I have not been able to use irsend (see above; irsend fails on opi; and pigpio does not work on Orange/Armbian), I hd to write my own code to transmit frames. This code transmits

  1. # cat transmit_ir.c

  2. // tcc -lwiringPi -run main.c

  3. #include <stdlib.h>
  4. #include <stdio.h>
  5. #include <fcntl.h>
  6. #include <sys/ioctl.h>
  7. #include <sys/time.h>
  8. #include <unistd.h>
  9. #include <wiringPi.h>

  10. int mydelay(long val)
  11. {
  12.         struct timeval t0, t1;
  13.         long elapsed,  count, loops;

  14.         elapsed=0;
  15.         gettimeofday(&t0,NULL);
  16.         for(; elapsed<val; )
  17.         {
  18. //              usleep(5);
  19.                 gettimeofday(&t1,NULL);
  20.                 elapsed = (t1.tv_sec-t0.tv_sec)*1000000 + t1.tv_usec-t0.tv_usec;
  21.         }
  22.         return(0);
  23. }

  24. main()
  25. {
  26. // https://stackoverflow.com/questions/5362577/c-gettimeofday-for-computing-time
  27.         int outled, res, pinstate;
  28.         outled=6;       // 6 = PA2 = pin 22
  29.         outled=7;       // 7 = PA6 = pin 7
  30.         char buf[9999];
  31.         int k;

  32. //      pinstate=INPUT;
  33.         pinstate=LOW;

  34.         wiringPiSetup () ;
  35.         pinMode (outled, OUTPUT);

  36.         usleep(1000);

  37.         while (1) {
  38.         while (res=scanf("%d", &k) ) {
  39.                 if(k==987654321) return (0);
  40.                 if(pinstate==LOW)
  41.                         pinstate=HIGH;
  42.                 else
  43.                         pinstate=LOW;
  44.         //      digitalWrite(i, HIGH);
  45.                 digitalWrite(outled, pinstate);
  46. //              digitalWrite(outled, LOW);
  47.                 mydelay(k);
  48.         }
  49.         digitalWrite(outled, LOW);
  50.         return (0);
  51. //      pinMode (outled, INPUT);
  52.         }

  53.         return (0);
  54. }
Copy code

WiringOP: http://www.orangepi.org/orangepi ... =148&extra=&page=15 for details. Installation:

  1. git clone <a class="moz-txt-link-freetext" href="https://github.com/zhaolei/WiringOP">https://github.com/zhaolei/WiringOP</a>
  2. cd WiringOP
  3. chmod +x build
  4. ./build
Copy code

I like tcc because it helps manipulating C code the same way as shell script; or, even closer to python. It allows building at run time; what means, when a project is already running, the code is live rebuild during execution or calling script; changes are immediate during development. I don't care about the performance loss in this case, because tcc is run only a few times per day, and it is run BEFORE the time critical operation, and thus, does not interact with the time critical parts.

This code reads stdin without checking any data validity. You must be 100% cerain of what you are sending.

When changing the pin mode from INPUT to OUTPUT, the chipset always sets the pin value to 0 by default. So, my first idea was to switch the pin mode between INPUT and OUTPUT, so that the pin changed between third state (free floating) and 0V. I found that this mode change takes time, and introduces lags that is not compatible with my timing requirements. If I could use the pin mode to send data, it means, I could directly connect the IR-DATA line to the GPIO, and don't care about anything.

Because I had to keep the pin in OUTPUT mode, it means I had to interface the PGIO with a transistor; thus, the hardware requirement described below.

I initially used PA2, which is in the middle of the port; then, I changed to PA6 to make hardware design easier. PA2 is too close to the SPI port. PA6 allows me to solder a rectangular board that does not take care at all about the SPI port.

In the end, the transmission hardware interface is a simple rectangle, and is easily removable.

There are several possible optimisations:
- replace the first line with soimething like " #!/usr/bin/tcc -lwiringPi -run " to allow running the file directly (see below in other messages)
- since we are doing time critical operations, it would be wise to use isolcpu (see below inside this message)

Selecting the output pin requires to understand the pin association table. It's given by this table:

  1. # gpio readall
  2. +-----+-----+----------+------+---+-Orange Pi+---+---+------+---------+-----+--+
  3. | BCM | wPi |   Name   | Mode | V | Physical | V | Mode | Name     | wPi | BCM |
  4. +-----+-----+----------+------+---+----++----+---+------+----------+-----+-----+
  5. |     |     |     3.3v |      |   |  1 || 2  |   |      | 5v       |     |     |
  6. |  12 |   8 |    SDA.0 | ALT3 | 0 |  3 || 4  |   |      | 5V       |     |     |
  7. |  11 |   9 |    SCL.0 | ALT3 | 0 |  5 || 6  |   |      | 0v       |     |     |
  8. |   6 |   7 |   GPIO.7 | ALT3 | 0 |  7 || 8  | 0 | ALT3 | TxD3     | 15  | 13  |
  9. |     |     |       0v |      |   |  9 || 10 | 0 | ALT3 | RxD3     | 16  | 14  |
  10. |   1 |   0 |     RxD2 | ALT3 | 0 | 11 || 12 | 0 | ALT3 | GPIO.1   | 1   | 110 |
  11. |   0 |   2 |     TxD2 | ALT3 | 0 | 13 || 14 |   |      | 0v       |     |     |
  12. |   3 |   3 |     CTS2 | ALT3 | 0 | 15 || 16 | 0 | ALT3 | GPIO.4   | 4   | 68  |
  13. |     |     |     3.3v |      |   | 17 || 18 | 0 | ALT3 | GPIO.5   | 5   | 71  |
  14. |  64 |  12 |     MOSI | ALT3 | 0 | 19 || 20 |   |      | 0v       |     |     |
  15. |  65 |  13 |     MISO | ALT3 | 0 | 21 || 22 | 0 | ALT3 | RTS2     | 6   | 2   |
  16. |  66 |  14 |     SCLK | ALT3 | 0 | 23 || 24 | 0 | ALT3 | CE0      | 10  | 67  |
  17. |     |     |       0v |      |   | 25 || 26 | 0 | ALT3 | GPIO.11  | 11  | 21  |
  18. |  19 |  30 |    SDA.1 | ALT3 | 0 | 27 || 28 | 0 | ALT3 | SCL.1    | 31  | 18  |
  19. |   7 |  21 |  GPIO.21 | ALT3 | 0 | 29 || 30 |   |      | 0v       |     |     |
  20. |   8 |  22 |  GPIO.22 | ALT3 | 0 | 31 || 32 | 0 | ALT3 | RTS1     | 26  | 200 |
  21. |   9 |  23 |  GPIO.23 | ALT3 | 0 | 33 || 34 |   |      | 0v       |     |     |
  22. |  10 |  24 |  GPIO.24 | ALT3 | 0 | 35 || 36 | 0 | ALT3 | CTS1     | 27  | 201 |
  23. |  20 |  25 |  GPIO.25 |  OUT | 1 | 37 || 38 | 0 | ALT3 | TxD1     | 28  | 198 |
  24. |     |     |       0v |      |   | 39 || 40 | 0 | ALT3 | RxD1     | 29  | 199 |
  25. +-----+-----+----------+------+---+----++----+---+------+----------+-----+-----+
  26. | BCM | wPi |   Name   | Mode | V | Physical | V | Mode | Name     | wPi | BCM |
  27. +-----+-----+----------+------+---+-Orange Pi+---+------+----------+-----+-----+
Copy code

The gpio command is not available in the raw Ubuntu image; it's provided by WiringOP.

*** CPU optimisations.

There are several things to tune for time critical operations.

The easy part (this will hugely increase the power consumption of the pi; it's not an issue for me, because during winter time, they contribute to hgeating the house; during summer time the whole system is turned off completely by a relay):

  1. /usr/bin/cpufreq-set -f 1200MHz
  2. /usr/bin/cpufreq-set -g performance
Copy code

The tricky part:
https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=200793

Add "isolcpus=3" to the kernel pompt arguments. This will prevent the normal system from using the core #3 (the first core is #0) for normal tasks; then, the core will be dedicated for what we explicitely want to, and will avoid having the core shared between several tasks. Our code will never be interrupted by an other task.

Editing kernel arguments is tricky on OrangePi/Armbian: edit the file /boot/boot.cmd , search for a line looking like

  1. setenv bootargs "root=${rootdev} rootwait rootfstype=${rootfstype} ${consoleargs} hdmi.audio=EDID:0 disp.screen0_output_mode=${disp_mode} panic=10 consoleblank=0 loglevel=${verbosity} ubootpart=${partuuid} ubootsource=${devtype} usb-storage.quirks=${usbstoragequirks} ${extraargs} ${extraboardargs} isolcpus=3"
Copy code

and run this command which is given in the last comment of the file (about line 44 in write_ir.sh ):

  1. mkimage -C none -A arm -T script -d /boot/boot.cmd /boot/boot.scr
Copy code

After reboot, type "top", then press the number "1", and you will see an expanded view of your activity; whatever you do, core #3 will always remain around 99% iddle time. A good command to check it and produce load is to open 6 terminals, and run this harmless command in each one: "cat /dev/urandom >/dev/null". Each of them will try tp use 100% CPU; they are started randomly on a core or an other; but now, they will not be started on core #3.

To start the transmission program on the desired core, change the line that calls transmit_ir into this:

  1. echo -e ${str} | /usr/bin/taskset -c 3 tcc -lwiringPi -run /root/transmit_ir.c
Copy code

Now, tcc and child process should all run on core #3. If you want to make things even cleaner, you may build a definitive binary

  1. tcc -lwiringPi transmit_ir.c -o transmit_ir
Copy code

and change the call for

  1. echo -e ${str} | /usr/bin/taskset -c 3 /root/transmit_ir
Copy code

7

threads

38

posts

181

credits

Registered member

Rank: 2

credits
181
 Author| Published in 2018-2-18 18:52:31 | Show all floors
Edited by DoubleHP at 2018-2-19 04:19

*** IR transmission hardware

This was the easiest part for me to design, and will be the most difficult to explain with words.

I initially wanted to directly wire the output GPIO pin, with the input IR pin of the opi, and to the IR-DATA line; use the INPUT state of GPIO to let the line float (rising +5V due to the pullup), and short circuit it to 0C by only setting the pin as OUTPUT, which by default goes to the value "0", as 0V on this chipset (I have tried many things, there is no way to turn a GPIO to '1' (+5V) at the moment we set a pin as OUTPUT - on this pi). But changing pin mode takes a huge amount of time, and is too slow for IR transmission.

This would have had the advantage of being simple, discrete, and invisible.

... So, I had to keep the pin in OUTPUT mode, and only change the value. Value 0 to leave the data line high, value 1 to grab it down (inverted logic could be possible, in an other use case).

The circuit is trivial: just use one transistor (BC548C; but any similar NPN will do), and a pair of resistors.

http://www.falstad.com/circuit/circuitjs.html?cct=$+1+0.000005+10.20027730826997+50+5+43%0Ar+112+192+208+192+0+1000%0Ar+256+160+256+80+0+10000%0At+224+192+256+192+0+1+0+0+100%0Ag+256+224+256+240+0%0A207+96+192+48+176+0+PA6%0A207+96+224+48+208+0+0V+pin+of+Opi%0A207+288+176+400+160+0+IR-DATA+line+of+air+cooling+terminal%0A207+288+224+384+240+0+0V+line+of+the+air+cooling+terminal%0A207+96+80+48+80+0++5V%0Aw+96+80+256+80+0%0Aw+256+160+256+176+0%0Aw+96+192+112+192+0%0Aw+96+224+256+224+0%0Aw+256+224+256+208+0%0Aw+256+224+288+224+0%0Aw+288+176+256+176+0%0Aw+256+160+96+160+0%0A207+96+160+48+144+0+IR+Input+pin+of+Opi%0Aw+224+192+208+192+0%0A
http://tinyurl.com/y9mr3qjd

The circuit allows to plug an IR receiver like
http://www.dx.com/p/vs1838b-remote-ir-receiving-module-deep-blue-295903#.Wohcl9Uhhpg (or any similar 3 leg IR receiver).

https://drive.google.com/file/d/ ... gJ/view?usp=sharing
https://drive.google.com/file/d/ ... QN/view?usp=sharing
https://drive.google.com/file/d/ ... IM/view?usp=sharing
https://drive.google.com/file/d/ ... Z9/view?usp=sharing
https://drive.google.com/file/d/ ... n1/view?usp=sharing
https://drive.google.com/file/d/ ... 1B/view?usp=sharing

The top of this PCB has two pins to connect to the IR-DATA and 0V of terminal. This top-hat board can be removed, and the project can be used naked, without the PCB.

The wires of the DS18B20 are soldered on a female header. Do not forget a 4kR or 10kR pullup resistor between data and v+ (here I use +3.3). This way, the temp probe can be attached and detached via a simple plug; be carefull about orientation; there is no notch to prevent mistakes; YOU have to remember that the black wire goes next to the ethernet connector. Transparent heat shrink tube helps making things a bit more rigid, and avoids shortcircuits.

*** execution

To run the project, simply chain the two scripts in a background service:

  1. ./read_ir.sh | ./write_ir.sh
Copy code

To send pre-recorded frames, touch the file corresponding to a template:
- to turn off: touch /dev/shm/frame_off
- to turn on: touch /dev/shm/frame_on_heat

I have planned that external calls may only send frames that are already programmed in write_ir.sh .

This project is a proxy because I catch frames on the fly, and may alter them. And send blocking orders.

It's not possible for me to modify the IR remotes; this global solution solves many problems at the same time.

7

threads

38

posts

181

credits

Registered member

Rank: 2

credits
181
 Author| Published in 2018-2-18 20:12:11 | Show all floors
Edited by DoubleHP at 2018-2-24 23:42

https://drive.google.com/file/d/ ... ln/view?usp=sharing
https://drive.google.com/file/d/ ... 76/view?usp=sharing
Brown wire goes to the gnd pin of TEST plug. On the red wire of the diffuser I use a vampire connector to catch 14V.

https://drive.google.com/file/d/ ... iz/view?usp=sharing
From the bottom plug which goes to the front LED pannel, the two top wires are 0V and IR-DATA

https://drive.google.com/file/d/ ... YZ/view?usp=sharing
The pi fits in the back part, in the angle; there is a very large room for much more than this. The supply sits in a middle channel, inside the corner. Once every thing is in place, it can all fit the cover; only the probe is kept on the side, so half of it's length can come out of the cover. I am not sure if I will leave the temp probe here; the wall is very cold, and this place is probably not representative of the room temp; I should probably move the probe further away from the wall.

https://drive.google.com/file/d/ ... NH/view?usp=sharing
Once closed, only the temperature probe is visible.

*** Glitches

I have found a glitch that produces 'nois', 'erratic bits' received by mode2, randomly. When terminal is off, I usually don't have any; when fans are blowing, I get 1 to 5 glitches per second. This introduces errors in frame decoding.

My best try was to remove the supply completelt, and power the opi from a battery; it did not affect the problem at all. Even when the pi was connected to the terminal with only two wires (ground and IR-DATA), I still had the glitches.

... untill I connected the ground to earth.

I do not understand the source of this glitch; and this direct connection is probably a bad idead; a better solution would probably to use an RC filter (R being about 1MR (be carefull to use a 500V resistor; most carbone resistors are 250V, and will arc on the high part of the sin wave around 380V), and C= 0.1uF X2 ('X2' as in "type hix two" aka self healing and 500V certified; not as in "twice")).

Edit: Directly connecting ground (0V output of any transformer) to earth (the green/yellow wire going outside under the garden) is heavily discouraged. Even for just a few seconds. The proper way to do things is to connect them with an X2 capacitor, usually 100nF (0.1uF) ("X2" being a "type", that means, certified for use on live mains 250V~ (400V peak), self healing; a better and more expensive type is called "Y"); and, in parallel to this capacitor, you MUST connect a resistor; it must meet many specifications:
- large enough so that it does not overheat (given 250V, U=RI + P=UI => P=U²/R => P=62500/R; R=62500/P, so a 0.25W resistor must be at least 250KR; 470KR is better)
- small enough to discharge the capacitor within 1s (at most 10s): t=RC => 1=470000C => C=1/470000 => at most 2uF; we usually use between 0.1uF and 1uF
- all X2 and Y capacitors are certified for 400V peak
- most carbone resistors are manufactured for 250V peak; so 400 V peak will damage them; many resistors are manufactured for 500V peak; but I don't know which ones (they are easy to find when you fully read the specs)

So, the most common recommended filter is 100nF or 220nF, in parallel with a pair of 270KR or 470KR (for a total of 540KR or 940KR).

People having mains at 110V can use exactly the same configuration, or simplify and use a single 470KR or 1MR resistor (110V~ gives 190V peak; any classic carbone resistor can do that).

I am talking about classic 1/4W ordinary resistor; becarefull, if you are used to collect used components from old stuff, there exist 1/8W carbone resistors that have not only a lower power rate, but also a lower max voltage. They are significantly smaller that classic resistors. Never use CMS resistors, as they are usually rated for 50V (between 25 and 125V).

*** TODO

- talk about multicast
- anti freeze wrapper

*** Final word

Around this code, there will be other scripts to manage the whole system remotely.

This installation contains 4 terminals; so, this setup is done 4 times. All pi's are connected together via Wi-Fi, and communicate via multicast.

7

threads

38

posts

181

credits

Registered member

Rank: 2

credits
181
 Author| Published in 2018-2-18 20:26:14 | Show all floors
*** Updates.

Reserved message for future updates.
You need to log in before you can reply login | Register

Points Rule

Quick reply Top Return list