forked from acemtp/homebridge-rpi-rts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
RpiGpioRts.js
190 lines (167 loc) · 5.95 KB
/
RpiGpioRts.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Requires https://github.com/joan2937/pigpio installed on the Raspberry Pi
const fs = require('fs');
const pigpio = require('pigpio');
// The Raspberry Pi's GPIO pin number linked to the 'data' pin of the RF emitter
const outPin = 4;
const output = new pigpio.Gpio(outPin, { mode: pigpio.Gpio.OUTPUT });
/**
* Class using Raspberry Pi GPIO to send waveform data
* to a 433.42 MHz RF emitter to simulate a Somfy RTS Remote
* to control roller shutters
*
* Somfy RTS protocole from https://pushstack.wordpress.com/somfy-rts-protocol/
* Implementation translated from Python https://github.com/Nickduino/Pi-Somfy
*
* @class RpiGpioRts
*/
class RpiGpioRts {
/**
* Constructor of the class RpiGpioRts
*
* @constructor
* @param {Function} log - Log function
* @param {Object} config - Config object
*/
constructor(log, config) {
this.log = log;
if (!config || !config.id) {
throw new Error(`Invalid or missing configuration.`);
}
this.id = parseInt(config.id, 10);
this.retrieveRollingCode();
this.buttons = {
My: 0x1,
Up: 0x2,
Down: 0x4,
Prog: 0x8,
};
}
/**
* Get the stored value of the rolling code
*
* @method retrieveRollingCode
*/
retrieveRollingCode() {
try {
const code = fs.readFileSync(`./${this.id}.txt`);
if (Number.isNaN(parseInt(code, 10))) {
this.rollingCode = 1;
this.log.debug(`No valid rolling code in file ./${this.id}.txt, set rolling code to 1`);
} else {
this.rollingCode = parseInt(code, 10);
this.log.debug(`Retrieved rolling code ${this.rollingCode} from file ./${this.id}.txt`);
}
} catch (err) {
if (err.code === 'ENOENT') {
this.rollingCode = 1;
this.log.debug(`No file ./${this.id}.txt, set rolling code to 1`);
} else {
throw err;
}
}
}
/**
* Store the value of the rolling code
*
* @method saveRollingCode
*/
saveRollingCode() {
fs.writeFile(`./${this.id}.txt`, this.rollingCode.toString(), err => { if (err) throw err; });
this.log.debug(`Saved rolling code ${this.rollingCode} in file ./${this.id}.txt`);
}
/**
* Return the payload data to send as a byte array
*
* @method getPayloadData
* @param {String} button - The button pressed: Up, Down, My, Prog
* @return {Array} - A byte array containing the payload data to send
*/
getPayloadData(button) {
const frame = [];
frame.push(0xA7); // [0] Encryption key. Doesn't matter much
frame.push(this.buttons[button] << 4); // [1] Button pressed? The 4 LSB will be the checksum
frame.push(this.rollingCode >> 8); // [2] Rolling code (big endian)
frame.push(this.rollingCode & 0xFF); // [3] Rolling code
frame.push(this.id & 0xFF); // [4] Remote address (little endian)
frame.push((this.id >> 8) & 0xFF); // [5] Remote address
frame.push(this.id >> 16); // [6] Remote address
// The checksum is calculated by doing a XOR of all bytes of the frame
let checksum = frame.reduce((acc, cur) => acc ^ cur ^ (cur >> 4), 0);
checksum &= 0b1111; // Keep the last 4 bits only
frame[1] |= checksum; // Add the checksum to the 4 LSB
// The payload data is obfuscated by doing an XOR between
// the byte to obfuscate and the previous obfuscated byte
for (let i = 1; i < frame.length; i++) {
frame[i] ^= frame[i - 1];
}
return frame;
}
/**
* Return the waveform to send to the emitter through the GPIO
*
* According to https://pushstack.wordpress.com/somfy-rts-protocol/
* 4 repetitions is enough, even for the Prog button
*
* @method getWaveform
* @param {Array} payloadData - A byte array containing the payload data to send
* @param {Number} repetitions - The number of time the frame should be sent
* @return {Array} - Waveform to send
*/
static getWaveform(payloadData, repetitions) {
const wf = [];
// Wake up pulse + silence
wf.push({ gpioOn: outPin, gpioOff: 0, usDelay: 9415 });
wf.push({ gpioOn: 0, gpioOff: outPin, usDelay: 89565 });
// Repeating frames
for (let j = 0; j < repetitions; j++) {
// Hardware synchronization
const loops = j === 0 ? 2 : 7;
for (let i = 0; i < loops; i++) {
wf.push({ gpioOn: outPin, gpioOff: 0, usDelay: 2560 });
wf.push({ gpioOn: 0, gpioOff: outPin, usDelay: 2560 });
}
// Software synchronization
wf.push({ gpioOn: outPin, gpioOff: 0, usDelay: 4550 });
wf.push({ gpioOn: 0, gpioOff: outPin, usDelay: 640 });
// Manchester enconding of payload data
for (let i = 0; i < 56; i++) {
if ((payloadData[parseInt(i / 8, 10)] >> (7 - (i % 8))) & 1) {
wf.push({ gpioOn: 0, gpioOff: outPin, usDelay: 640 });
wf.push({ gpioOn: outPin, gpioOff: 0, usDelay: 640 });
} else {
wf.push({ gpioOn: outPin, gpioOff: 0, usDelay: 640 });
wf.push({ gpioOn: 0, gpioOff: outPin, usDelay: 640 });
}
}
// Interframe gap
wf.push({ gpioOn: 0, gpioOff: outPin, usDelay: 30415 });
}
return wf;
}
/**
* Prepare and send a command through the GPIO
*
* @method sendCommand
* @param {String} button - The button pressed: Up, Down, My, Prog
* @return {Array} - An array containing the services
*/
sendCommand(button) {
this.log.debug(`Function sendCommand with id: ${this.id}, rolling code: ${this.rollingCode} and button: ${button}`);
const payloadData = this.getPayloadData(button);
const waveform = RpiGpioRts.getWaveform(payloadData, 4);
// Sending waveform
output.digitalWrite(0);
pigpio.waveClear();
pigpio.waveAddGeneric(waveform);
const waveId = pigpio.waveCreate();
if (waveId >= 0) {
pigpio.waveTxSend(waveId, pigpio.WAVE_MODE_ONE_SHOT);
}
while (pigpio.waveTxBusy()) { /* empty */ }
pigpio.waveDelete(waveId);
// Incrementing rolling code and storing its value for next time
this.rollingCode++;
this.saveRollingCode();
}
}
module.exports = RpiGpioRts;