Thursday, December 26, 2013

More on observing the 433MHz band, Nexus IW004

One of the Christmas gifts in the family this year was a weather station, "Nexus IW004 / 36-5136". Some 'googling' of the brand got me to one of the local gadget stores in the region, Clas Ohlson. It is capable of measuring indoor and outdoor temperature and humidity, air pressure and the date. It also predicts the upcoming weather based on these data. The outdoor unit includes a sensor with mounting possibilities attached to the unit with a 1 meter wire.

Analysing the data with a 433 MHz receiver unit and a logic analyser showed a burst of twelve identical signals, 3.9 ms apart, for each reading of outdoor status. For channel 1 these bursts were located 56.95 seconds apart. For channel 2, 67.05 seconds and for channel 3 84.97 seconds apart.

Each pulse is 0.47 ms wide, with 1.98 ms separating 'ones' and 0.99 ms separating 'zeros'. The last pulse in a 'burst' is narrower, 0.25 ms wide.


Using this information and by varying the environmental parameters the following table could be compiled:


bit  1 - 10  Sensor ID and battery status(?)
bit 11 - 12  Channel ID
bit 13 - 24  Temperature * 10 in two's complement notation
bit 25 - 28  Always 1
bit 29 - 36  Humidity


Something also happens in bit 3 - 7 when the channel changes on the outdoor sensor.

Friday, July 26, 2013

Using the Nexa remote together with a tellstick device

One of my Nexa switches is operated programmaticly from a computer via a tellstick. Whenever I want to control this device manually I will log in to the computer and issue the proper commands to operate the switch. However, not everybody finds this procedure acceptable so I needed a way to operate the switch both from the tellstick and from a Nexa remote.

First I reprogrammed the Nexa switch to accept the Nexa remote.

Then, by using the hard- and software described in decoding-new-nexa-protocol I could extract the ID code sent from the Nexa remote. This ID code could be directly entered into the "/etc/tellstick.conf" file and after a restart of the tellstick daemon the switch were operable by both the Nexa remote and the tellstick.

Tuesday, May 28, 2013

Using a MCE remote control with mythtv / xbmc


No lircd please.

After five years of mythtv 0.19 / xbmc (no udates of any kind during that time) and numerous restarts of lirc I got myself a new remote with an usb reciever, "MCE remote". A remote that shows up directly in the /dev/input directory of ubuntu 12.04. In addition to some standard buttons it also got a built in mouse. If one presses and holds down one of the numeric buttons it first emit a digit and then, after a while, emit a backspace and a character. E.g: Holding down "2" gives "2", delay, "<bs>A", delay, <bs>B, etc.

All very handy, but in the end I will probably use the reciever with my learning remote control wich also controls the amplifier and other stuff. 



It was bought under the brand name "Fractal design", but as ubuntu concerns this is the important stuff:
(since ubuntu 12.04 lists this device in /dev/input/by-id it is easy to locate, otherwise one could plug/unplug and look in the /dev/input/ directory)

sudo udevadm info -a --name=/dev/input/by-id/usb-Cypress_Cypress_USB_Keyboard-event-mouse 

Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.

  looking at device '/devices/pci0000:00/0000:00:02.0/usb3/3-1/3-1:1.0/input/input2/event2':
    KERNEL=="event2"
    SUBSYSTEM=="input"
    DRIVER==""

  looking at parent device '/devices/pci0000:00/0000:00:02.0/usb3/3-1/3-1:1.0/input/input2':
    KERNELS=="input2"
    SUBSYSTEMS=="input"
    DRIVERS==""
    ATTRS{name}=="Cypress Cypress USB Keyboard"
    ATTRS{phys}=="usb-0000:00:02.0-1/input0"
    ATTRS{uniq}==""
    ATTRS{properties}=="0"

  looking at parent device '/devices/pci0000:00/0000:00:02.0/usb3/3-1/3-1:1.0':
    KERNELS=="3-1:1.0"
    SUBSYSTEMS=="usb"
    DRIVERS=="usbhid"
    ATTRS{bInterfaceClass}=="03"
    ATTRS{bInterfaceSubClass}=="01"
    ATTRS{bInterfaceProtocol}=="01"
    ATTRS{bNumEndpoints}=="01"
    ATTRS{supports_autosuspend}=="1"
    ATTRS{bAlternateSetting}==" 0"
    ATTRS{bInterfaceNumber}=="00"

  looking at parent device '/devices/pci0000:00/0000:00:02.0/usb3/3-1':
    KERNELS=="3-1"
    SUBSYSTEMS=="usb"
    DRIVERS=="usb"
    ATTRS{bDeviceSubClass}=="00"
    ATTRS{bDeviceProtocol}=="00"
    ATTRS{devpath}=="1"
    ATTRS{idVendor}=="04b4"
    ATTRS{speed}=="1.5"
    ATTRS{bNumInterfaces}==" 1"
    ATTRS{bConfigurationValue}=="1"
    ATTRS{bMaxPacketSize0}=="8"
    ATTRS{busnum}=="3"
    ATTRS{devnum}=="2"
    ATTRS{configuration}==""
    ATTRS{bMaxPower}==" 98mA"
    ATTRS{authorized}=="1"
    ATTRS{bmAttributes}=="a0"
    ATTRS{bNumConfigurations}=="1"
    ATTRS{maxchild}=="0"
    ATTRS{bcdDevice}=="0100"
    ATTRS{avoid_reset_quirk}=="0"
    ATTRS{quirks}=="0x0"
    ATTRS{version}==" 1.10"
    ATTRS{urbnum}=="12"
    ATTRS{manufacturer}=="Cypress"
    ATTRS{removable}=="unknown"
    ATTRS{idProduct}=="0100"
    ATTRS{bDeviceClass}=="00"
    ATTRS{product}=="Cypress USB Keyboard"

In order to remap the remote keys we have to write a keymaps file and run this when the usb reciever are plugged into the system. A great "howto" on this is in writing udev rules. As shown by the '/dev/by-id' contents, the remote control is two units; the mouse part and the rest. The difference between these are the 'KERNEL==' attribute. Since one can use info from the device itself and one of the parent devices I used this rule:

SUBSYSTEM=="input", KERNEL=="event*", ATTRS{idVendor}=="04b4", ATTRS{idProduct}=="0100",  RUN+="/bin/sh -c 'echo $name >> /tmp/test.udev"
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{idVendor}=="04b4", ATTRS{idProduct}=="0100",  RUN+="keymap $name cypressusb"

This is the method I use to convince myself that the udev rules are hit. Just check the '/tmp/test.udev' for which devices that match the rule. '/lib/udev/keymap device keymapfile' (where 'keymapfile' is located in /lib/udev/keymaps/ updates the keymap of the device). To simulate adding devices use

sudo udevadm trigger


'ir-keytable' may be used to check the keycodes emitted from the remote:

#From ir-keytable -t -d /dev/input/by-id/usb-Cypress_Cypress_USB_Keyboard-event-mouse

#Pressing buttons from top left across to the right and down

700e0 KEY_LEFTCTRL (0x001d) 
70015 KEY_R (0x0013)

c00b7 KEY_STOPCD (0x00a6)

700e2 KEY_LEFTALT (0x0038)
7003d KEY_F4 (0x003e)
**
700e0 KEY_LEFTCTRL (0x001d)
70005 KEY_LEFT (0x0069)

700e0 KEY_LEFTCTRL (0x001d)
700e1 KEY_LEFTSHIFT (0x002a)
70013 KEY_P (0x0019)

700e0 KEY_LEFTCTRL (0x001d)
700e1 KEY_LEFTSHIFT (0x002a)
70005 KEY_LEFT (0x0069)
**
700e0 KEY_LEFTCTRL (0x001d)
700e1 KEY_LEFTSHIFT (0x002a)
70005 KEY_LEFT (0x0069)

700e0 KEY_LEFTCTRL (0x001d)
70013 KEY_P (0x0019)

700e0 KEY_LEFTCTRL (0x001d)
700e1 KEY_LEFTSHIFT (0x002a)
70009 KEY_RIGHT (0x006a)
**

700e0 KEY_LEFTCTRL (0x001d)
700e1 KEY_LEFTSHIFT (0x002a)
70010 KEY_M (0x0032)

700e2 KEY_LEFTALT (0x0038)
700e3 KEY_LEFTMETA (0x007d)
70028 KEY_ENTER (0x001c)

700e0 KEY_LEFTCTRL (0x001d)
7000a KEY_G (0x0022)
**
70028 KEY_ENTER (0x001c)

7002a KEY_BACKSPACE (0x000e)

70050   KEY_LEFT
70052   KEY_UP
7004f   KEY_RIGHT
70051   KEY_DOWN

then the mouse buttons 

left mouse btn  90001  BTN_MOUSE (0x0110)
right mouse btn 90002  BTN_RIGHT (0x0111)

and joystick
and continuing...

RecTV
700e0   KEY_LEFTCTRL
70012   KEY_O

Vol+
70043   KEY_F10
Vol-
70042   KEY_F9

Ch/Pg+
7004b   KEY_PAGEUP
Ch/Pg-
7004e   KEY_PAGEDOWN

LiveTV
700e0   KEY_LEFTCTRL
70017   KEY_T
**

S1   c0223  KEY_HOMEPAGE
S2   c022a  KEY_BOOKMARKS
mute 70041  KEY_F8
S3   c0224  KEY_BACK
S4   c0225  KEY_FORWARD

red     700e0  KEY_LEFTCTRL
        700e1  KEY_LEFTSHIFT
        70017  KEY_T
green   700e0  KEY_LEFTCTRL
        70008  KEY_E
yellow  700e0  KEY_LEFTCTRL
        7000c  KEY_I
blue    700e0  KEY_LEFTCTRL
        70010  KEY_M

the numeric keys, and finally...;
clear   7002a  KEY_BACKSPACE (0x000e)
enter  70028 KEY_ENTER (0x001c)

The remote dublicates the keycodes sent from the "back" and "clear" buttons and also the "ok" and "enter" buttons.

Mythtv uses <ESC> as back key, but I don't want to reassign the 'back' key on the remote from backspace, it could be needed. Therefore the first iteration of the keymap would be to reassign left and right mouse button to OK and ESC.

/lib/udev/keymaps/cypressusb 
0x90001  OK     # left mouse btn -> OK
0x90002  ESC    # right mouse btn -> ESC




Wednesday, January 16, 2013

Decoding the "new" NEXA protocol


Description of the NEXA protocol


One burst of data from a NEXA remote.


The "new" NEXAprotocol are described here. Basically it is like this: The signal for start TX is a pulse (0.2 ms) followed by a delay of 2.7 ms. A pulse followed by a short delay (0.4 ms) reads as "0". A pulse followed by a longer delay (1.4 ms) reads as "1". The data are then combined into a 32 bit string by assigning "01" as '0' and "10" as '1'.

00000000001000000000000010010010

The sequence above were generated from a tellstick using "house=32770" and "unit=3".
The first 26 bits defines the ID of the remote as a binary number. If set, the 27th bit defines the command active for all recievers with the ID ( group command). The 28th bit are the command, '1' for "ON", '0' for "OFF". The last four bits are the unit code. For my NEXA remote control "unit 1" are "0000", "unit 2" are "0001" and "unit 3" are "0010". Every push on the remote generates a train of four pulse bursts, the tellstick generated five.

Decoding

It is noisy in the 433 MHz range and hard to get the first burst of data.
It appears that the 433 MHz reciever used are doing level adjustment based on the signal level in the 433 MHz range so the strategy for initiating the decoding were to wait until some data forced the reciever to turn down the gain.

An Atmega 328P available at hand were chosen to do the decoding. The 433 MHz RX data pin were connected to one of the PCINT22 interrupt pins, PD6. The 16 bit timer1 were used to time pulse widths and delays. Power to the 433 MHz RX unit were provided from PD5 (GND) and PD7 (5V). PC5 were used for debugging by outputting pulses to a logic analyzer. Data were outputted to a LCD screen using code from extremeelectronics.

The 328P were run at 8 MHz. A check for how slow it can run and still decode the data are left for later.

 /*  
  * WirelessReader.c  
  *  
  * Sample signals from 433 MHz reciver and decode in a state machine.  
  *   
  *    PCB: SousVide V.1  
  *    Using Using port 11, 12 and 13 (PD5, PD6 and PD7) for 433 MHz RX, drawing ~4 mA  
  *  
  *  
  *  
  *  
  *  
  */   
 #define F_CPU 8000000UL  
 #include <avr/io.h>  
 #include <avr/interrupt.h>  
 #include "lcd.h"  
 #define T01 1  
 #define T10 0  
 // shortest and longest accepted pulsewidth  
 #define kMinPulseWidth 140  
 #define kMaxPulseWidth 300 //210  
 // Minimum time between two pulsetrains  
 #define kPulseTrainDelay 9700  
 // Minimum time between init pulse and data stream  
 #define kMinInitDelay 2200 //2600  
 #define kMaxInitDelay 2800  
 // Maximum time between two data pulses  
 #define kMaxDataDelay 1450 //1363  
 // A delay longer than reads as a '1'  
 #define kZeroDelay 687 //346  
 // The data arrives in pairs, '01' or '10', ministate  
 // keeps track of these pairs  
 //    -1    undefined, waiting for '0' or '1'  
 //     0    '0' read, waiting for '1'  
 //     1    '1'    read, waiting for '0'  
 volatile int8_t ministate = -1;  
 //#state  
 //# 0 - wait for 9.3 ms (kPulseTrainDelay) silence  
 //# 1 - wait for and measure init pulse width  
 //# 2 - measure init delay  
 //# 3 - check data pulse width and start measuring data delay  
 //# 4 - read data delay and determine '0', '1'. n time out store data  
 volatile uint8_t state = 0;  
 volatile uint32_t rawdata, data;  
 volatile uint8_t dataready;  
 //PD5, ground for 433 MHz RX  
 #define RX_GND_PIN PD5  
 #define RX_DTA_PIN PD6  
 #define RX_VCC_PIN PD7  
 int main(void)  
 {  
 // Set up interrupts  
     PCMSK2 |= (1<<PCINT22);  
     PCICR |= (1<<PCIE2);  
 // Set up timer, do not start it  
     TCCR1B |= (1 << WGM12); //CTC mode  
     TIMSK1 |= (1<<OCIE1A);  
     state = 0;  
     rawdata = 0;  
     data = 0;  
     dataready = 0;  
 // Set up 433MHz RX  
     DDRD |= (1 << RX_GND_PIN) | (1 << RX_VCC_PIN);  
     DDRD &= ~(1<<RX_DTA_PIN);  
     PORTD &= ~(1 << RX_GND_PIN); // set to 0V  
     PORTD |= (1 << RX_VCC_PIN);    // set to 5V  
 //    PORTD |= (1 << RX_DTA_PIN); // pullup  
 // For debugging  
     DDRC |= (1 << PC5);  
 //Initialize LCD module  
     LCDInit(LS_NONE);//(LS_BLINK|LS_ULINE);  
     LCDClear();  
     sei();  
   while(1)  
   {  
     if (dataready==1)  
     {  
         cli();  
         char buff[8];  
         uint8_t tmp = data &0x000000FF;  
         sprintf(buff,"%04X", (data>>(16+6))&0x0000FFFF);  
         LCDWriteStringXY(0,0,buff);  
         sprintf(buff,"%04X", (data>>6)&0xFFFF);  
         LCDWriteStringXY(4,0,buff);  
         if (tmp & 0x10)  
         {  
             LCDWriteStringXY(0,1,"ON");  
         }  
         else  
         {  
             LCDWriteStringXY(0,1,"OFF");  
         }  
 //        LCDWriteStringXY(4,1,"U ");  
 //        LCDWriteIntXY(6,1,(tmp&0x0f),3);  
          if ((tmp & 0x0F) == 0x00) LCDWriteStringXY(4,1,"U 1");  
          if ((tmp & 0x0F) == 0x01) LCDWriteStringXY(4,1,"U 2");  
          if ((tmp & 0x0F) == 0x02) LCDWriteStringXY(4,1,"U 3");  
         if (tmp & 0x20) LCDWriteStringXY(4,1,"Grp");  
         sei();  
         for (int ii=0;ii<100;ii++)  
         {  
             _delay_ms(30);  
         }  
         dataready = 0;  
         cli();  
         LCDClear();  
         sei();  
     }  
   }  
 }  
 //reset state to initial  
 void inline resetstate()  
 {  
     unsigned char sreg;  
     state=0;  
     ministate=-1;  
     data = 0;  
     rawdata = 0;  
     TCCR1B &= ~(1 << CS11); //stop timer 1  
     sreg = SREG; // save global interrupt flag  
     cli();  
     TCNT1 = 0;  
     SREG = sreg; // restore interrrupt flag  
 }  
 // start timer  
 void inline starttimer(int v)  
 {  
     unsigned char sreg;  
     sreg = SREG; // save global interrupt flag  
     cli();  
     TCNT1 = 0;  
     OCR1A = v;  
     SREG = sreg; // restore interrrupt flag  
     TCCR1B |= (1 << CS11); //prescale 8  
 }  
 // This interrupt is called when the 433 MHz RX pin changes state  
 ISR(PCINT2_vect)  
 {  
     char transition;  
 // determine the state change direction, 0->1 or 1->0  
     if (PIND & (1<<PIND6))transition = T01;  
     else transition = T10;  
 //state 0: should time out, we need 9.3 ms silence, otherwise try again  
      if (state == 0)  
      {  
         starttimer(kPulseTrainDelay-100);  
          return;  
      }  
 // state 1: we had a 9.3 ms delay, now we are recieving the init pulse  
 // start the pulsewidth timer  
     if ((state==1) && (transition == T01))  
     {  
         starttimer(kMaxPulseWidth);  
         return;  
     }  
     if ((state==1) && (transition == T10))  
     {  
         if (TCNT1<kMinPulseWidth)  
         {  
             resetstate();  
             return;  
         }  
         else // pulse OK, too long and it will time out in the timer part  
         {  
             state = 2;  
             starttimer(kMaxInitDelay);  
             return;  
         }  
     }  
 // state 2: measure the init delay  
     if (state == 2)// && (transition == T01))  
     {  
         if (transition == T10)  
         {  
             resetstate();  
             return;  
         }  
         if (TCNT1<kMinInitDelay) // to short  
         {  
             resetstate();  
             return;  
         }  
         else // delay OK, too long and it times out  
         {  
             starttimer(kMaxPulseWidth);  
             rawdata = 0;  
             state = 3;  
             return;  
         }  
     }  
 //state 3: check pulsewidth in data transmission  
     if ((state == 3))// && (transition==T10))  
     {  
         if (TCNT1<kMinPulseWidth)  
         {  
             resetstate();  
             return;  
         }  
         else // pulse OK, too long and it will time out from the timer  
         {  
             starttimer(kMaxDataDelay);  
             state = 4;  
             return;  
         }  
     }  
 //state 4: read data, '0' or '1'  
     if (state == 4)  
     {  
         int t = TCNT1;  
         //observing 1310 or 320  
         if (t > kZeroDelay) // we read '1'  
         {  
             if (ministate==1) // error, two '1' in a row  
             {  
                 resetstate();
                 return();
             }  
             else if (ministate==-1) // OK, next bit should be '0'  
             {  
                 ministate = 1;  
             }  
             else if (ministate==0) // OK, bit pair completed  
             {  
                 ministate = -1;  
                 rawdata = (rawdata<<1);  
                 // no need to add '0'  
             }  
         }  
         else // we read '0'  
         {  
             if (ministate==0) // error, two '0' in a row  
             {  
                 resetstate();
                 return();
             }  
             else if (ministate==-1) // OK, next bit should be '1'  
             {  
                 ministate = 0;  
             }  
             else if (ministate==1) // OK, bit pair completed  
             {  
                 ministate = -1;  
                 rawdata = (rawdata<<1);  
                 rawdata +=1;  
             }  
         }  
         state = 3;  
         starttimer(kMaxPulseWidth);  
         return;  
         }  
 }  
 ISR(TIMER1_COMPA_vect)  
 {  
     if (state==0)  
     {  
         state = 1;  
         TCCR1B &= ~(1 << CS11); //stop the timer  
         return;  
     }  
 // pulsewidth/delay were to long  
     if ((state == 1) || (state == 2) || (state == 3))  
     {  
 PORTC = 0b00100000;  
 PORTC = 0b00000000;  
 _delay_us(5);  
 PORTC = 0b00100000;  
 PORTC = 0b00000000;  
         resetstate();  
         return;  
     }  
 // end of data burst, wait for next burst  
     if (state==4)  
     {  
         data = rawdata;  
         rawdata = 0;  
         dataready=1;  
         starttimer(kPulseTrainDelay>>1);  
         state = 0;  
     }  
 }  

This extracts the data in the pulsetrain, but it does not do any accumulation of the data in the pulse trains. Usaully there are at least four bursts that contains the same data.