-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrf433.cpp
More file actions
445 lines (399 loc) · 18.1 KB
/
rf433.cpp
File metadata and controls
445 lines (399 loc) · 18.1 KB
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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
/*
Dooya protocol
Different protocols have different pulse duration, for Dooya it is 350 microseconds
- look for HIGH separation duration (13 pulses) ending on a LOW (first phase of sync)
followed by a LOW separation duration (4 pulses) ending on a HIGH (second phase of sync)
- Then each bit is 4 pulse durations:
- Long (pulse duration x 2) is digit 1
- Short (pulse duration x 1) is digit 0
- When have 48 bits, message is complete
- For each command, it is repeated a number of times to ensure reception,
with a gap between each message (for Dooya remote is 5 repeats with gaps of 8ms)
The Dooya remote outputs 48 bits, but the last 8 bits are not needed.
The Dooya remote outputs a second message type for Extend, Retract & LED,
but this second message type can be ignored - could be to allow controller to also
be used for a different motor type.
The Dooya remote also outputs 8 trailing bits after the command bits but these
dont need to be transmitted (ie only 40 bit output needed)
Unknown protocol:
Cheap 433MHz devices use OOK (On-Off Keying) RF mode, so LOW is the default state when device is inactive.
In UNKNOWN_RX mode the app can capture the raw HIGH / LOW durations from messages
sent by the remote.
The resulting durations are displayed as HIGH / LOW pairs for manual analysis and decoding:
- A particular long LOW duration can indicate a message separation
- Inital HIGH + LOW durations after a message separation are longer and usually indicate
a message start preamble / sync
If preamble is not an even number of level changes, the resulting data will be misinterpreted
- Data bits encoding types:
- PWM (Pulse-Width Modulation (PWM): Total duration of each HIGH/LOW pair is the same,
where one duration is longer than the other:
eg: 750:250,300:700 - long HIGH is logic 1, short HIGH is logic 0
- PMM (Pulse-Position Modulation): Total duration of each HIGH/LOW pair is different,
but the duration of the LOW is similar
eg: 750:200,400:200 - long HIGH is logic 1, short HIGH is logic 0
- Manchester: The duration of each pair is not relevant, it is the transition between
logic levels that matter
eg: 500:500,400:300 - HIGH to LOW is logic 0, LOW to HIGH between first and second pair is logic 1
- Tristate: Uses multiple HIGH/LOW pairs to represent three different states
- Inverse: Any of the above where the interpretation of the logic levels is inverted
For the purposes of sniffing an unknown device (eg a remote control) to capture the different
bitstream per command for replay, it doesnt matter what the protocol is, just
transmit the captured bitstream with the same HIGH / LOW durations
The 433MHz receiver needs more power than is available from ESP 5V.
*/
#include "appGlobals.h"
// setup
#define BUF_SIZE (8 * 1024) // maximum number of pulses that can be captured
#define DOOYA_RPT 5 // Dooya default is each command message repeated 5 times
int txPin; // output pin connected to transmitter
int rxPin; // interrupt pin connected to receiver
int ledPin; // display HIGH / LOW input and output
int extendPin; // input pin to extend canopy
int stopPin; // input pin to stop canopy
int retractPin; // input pin to retract canopy
int lightPin; // input pin to toggle canopy LEDs on / off
int p2Pin; // input pin for programming remote
// DOOYA_RX: sniff Dooya protocol
// UNKNOWN_RX: capture / decode unknown protocol
// DOOYA_TX: transmit Dooya protocol represented as bitstream
// CUSTOM_TX: transmit custom protocol using captured HIGH/LOW durations
opModes opMode = NONE;
// Dooya protocol - PWM fixed time pulse
// signal lengths are multiples of pulse length
// a data bit is a HIGH and LOW signal totalling 4 pulses
// Dooya protocol constants, modifiable on web page by user
int pulseLen; // pulse length in microseconds
int syncHighPulses; // number of high pulse durations in sync message
int syncLowPulses; // number of low pulse durations in sync message
int longSignalPulse; // number of pulse durations in long data bit
int shortSignalPulse; // number of pulse durations in short data bit
int bitLen; // total data bits in a message
int msgGap; // separation (ms) between each message sent in command sequence
int minSignal; // signals in us below this value are spurious
// generic constants
#define SYNC_HIGH (syncHighPulses * pulseLen) // sync HIGH duration
#define SYNC_LOW (syncLowPulses * pulseLen) // sync LOW duration
#define SHORT_SIGNAL (shortSignalPulse * pulseLen) // bit short duration
#define LONG_SIGNAL (longSignalPulse * pulseLen) // bit long duration
#define SPLIT_SIGNAL ((SHORT_SIGNAL + LONG_SIGNAL) / 2)
#define MIN_SIGNAL (SHORT_SIGNAL / 2) // min duration of valid signal
#define MAX_SIGNAL (LONG_SIGNAL + SHORT_SIGNAL) // max duration of valid signal
#define TOLERANCE (1 * pulseLen)// when measuring duration
#define LONG_GAP OneMHz // 1 second as usecs
static volatile bool processingData = false; // whether in process of processing captured data
static uint32_t signalDurations[BUF_SIZE]; // HIGH and LOW durations of received signal data
static uint32_t pulseDurations[2][100]; // HIGH and LOW durations of transmitted messages
static hw_timer_t* longGapTimer = NULL;
static volatile int thisAction = NO_ACTION;
static volatile uint32_t cmdPriority = 0; // can be use to override current task processing
static TaskHandle_t receiveTaskHandle = NULL;
static TaskHandle_t transmitTaskHandle = NULL;
/************* constants for sending Dooya messages ****************/
char deviceId[25] = {0}; // 24 bit ID of device being controlled by remote
char channelData[9] = {0}; // 8 bits, first 4 bits is channel number (of 15), final bit indicates if multichannel
char commandBits[END_CMDS][9] = {0}; // 8 bit commands: stop current action, toggle LED on or off, extend awning, retract awning, programming remote / canopy command
char trailingBits[END_CMDS][9] = {0}; // 8 bit trailer after command bits, not needed
static const char* cmdStr[END_CMDS] = {"NONE", "EXTEND1", "EXTEND2", "STOP", "RETRACT1", "RETRACT2", "TOGGLE_LED1", "TOGGLE_LED2", "P2_COMMAND"};
/************* Receive and store signals ********************/
static inline void IRAM_ATTR wakeTask(TaskHandle_t thisTask) {
// utility function to invoke task from pin interrupt
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(thisTask, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
static void IRAM_ATTR longGapISR() {
// interrupt when timeout occurs on gap after messages
// dont process interrupts while displaying captured signals
if (!processingData) {
processingData = true;
wakeTask(receiveTaskHandle);
}
}
void controlLongGapTimer(bool restartTimer) {
// stop current timer
if (longGapTimer) {
timerDetachInterrupt(longGapTimer);
timerEnd(longGapTimer);
longGapTimer = NULL;
}
if (restartTimer) {
// (re)start timer interrupt for required interval
longGapTimer = timerBegin(OneMHz);
if (longGapTimer) {
timerAttachInterrupt(longGapTimer, &longGapISR);
timerAlarm(longGapTimer, LONG_GAP, true, 0); // as micro seconds
} else LOG_ERR("Failed to setup longGapTimer");
}
}
static void IRAM_ATTR handleInterrupt() {
// dont process interrupts while displaying captured signals
if (!processingData) wakeTask(receiveTaskHandle);
}
static bool storeSignalDuration(int thisLevel, int signalIndex) {
// determine signal duration
static bool gotHigh = false; // message sequence starts with a HIGH
static uint64_t startTime = micros(); // init only
uint64_t endTime = micros();
uint32_t signalDuration = (uint32_t)(endTime - startTime);
bool validSignal = false;
// check getting valid HIGH LOW sequences
if (signalDuration > minSignal && ((thisLevel == HIGH && !gotHigh) || (thisLevel == LOW && gotHigh))) {
signalDurations[signalIndex] = signalDuration;
gotHigh = !gotHigh;
if (signalIndex % 32 == 0) logPrint("."); // progress indicator
validSignal = true;
}
// reset for next signal
startTime = endTime;
return validSignal;
}
/********************* Decode & display Unknown protocol **********************/
static void displayRawData(uint32_t dataStart, uint32_t dataBits) {
char bitStream[200] = {0};
char* p = bitStream;
size_t remaining = sizeof(bitStream) - 1;
logPrint("Bit duration HIGH:LOW (us):\n");
for (uint32_t i = 0; i < dataBits; i++) {
if (i > 0 && (i % 16 == 0)) {
logPrint("%s\n", bitStream);
p = bitStream;
remaining = sizeof(bitStream) - 1;
}
int written = snprintf(p, remaining, "%lu:%lu, ", signalDurations[dataStart + (i * 2)], signalDurations[dataStart + (i * 2) + 1]);
if (written < 0 || (size_t)written >= remaining) {
LOG_ERR("Buffer bitStream overflow");
bitStream[0] = 0;
break;
}
p += written;
remaining -= written;
}
if (strlen(bitStream)) logPrint("%s\n", bitStream);
logLine();
}
static void decodeUnknown(uint32_t signalIndex) {
// output captured data as a single stream
displayRawData(0, signalIndex/2);
// user to add own code if required after manual decode
}
/********************* Decode & display Dooya Messages **********************/
static void displayDooyaData(uint32_t startPt, uint32_t endPt) {
// display meta data and content of received message
// durations comprise: message gap, HIGH sync, LOW sync, (HIGH data, LOW data) * 48
logPrint("Msg gap (ms): %lu\n", signalDurations[startPt] / 1000);
uint32_t msgDuration = 0;
// calculate message duration, ignoring last LOW as this continues as message gap
for (uint32_t i = startPt + 1; i < endPt - 1; i++) msgDuration += signalDurations[i];
logPrint("Msg duration (ms): %lu\n", msgDuration / 1000); //
// output Dooya protocol format as durations
logPrint("Sync duration HIGH:LOW (us): %lu:%lu\n", signalDurations[startPt + 1], signalDurations[startPt + 2]);
uint32_t dataOffset = 3;
uint32_t dataStart = startPt + dataOffset;
uint32_t dataBits = (endPt - dataStart) / 2;
displayRawData(dataStart, dataBits);
// output Dooya protocol format as bitstream
// Data bits - check duration of HIGH
// - each bit comprises 3 pulse lengths
// - Binary 1 represented by LONG_SIGNAL: HIGH x 2 then LOW
// - Binary 0 represented by SHORT_SIGNAL: HIGH then LOW x 2
// SIGNAL_SPLIT used to distinguish between LONG_SIGNAL and SHORT_SIGNAL
char bitStream[dataBits] = {0};
uint8_t j = 0;
for (uint8_t i = 0; i < dataBits + 1; i++) {
if (i == 24 || i == 32 || i == 40 || i == 48) {
bitStream[j] = 0;
if (i == 24) logPrint("Device ID: %s\n", bitStream);
else if (i == 32) logPrint("Channel Num: %s\n", bitStream);
else if (i == 40) logPrint("Command: %s\n", bitStream);
else if (i == 48) logPrint("Trailing: %s\n", bitStream);
j = 0;
}
// skip LOWS
bitStream[j++] = signalDurations[dataStart + (i * 2)] > SPLIT_SIGNAL ? '1' : '0';
}
logLine();
if (dataBits != bitLen) LOG_WRN("Invalid number of bits: %lu, should be: %u", dataBits, bitLen);
logLine();
}
static void decodeDooya(uint32_t endOfData) {
// decode and display Dooya messages
bool haveMsgs = false;
uint32_t startPt = 0;
// get each message span in turn
for (uint32_t i = 0; i < endOfData; i++) {
if (signalDurations[i] > (msgGap * 1000) - TOLERANCE) {
if (haveMsgs) {
// end of a message
displayDooyaData(startPt, i + 1);
// reset for next message
startPt = i;
} else {
haveMsgs = true;
startPt = i; // start of first message
}
}
}
}
/******************* Decoding control ********************/
static void receiveTask(void *parameter) {
// process interrupts (signal input or timeout)
static uint32_t signalIndex = 0;
while (true) {
// save incoming signals until stopped
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if (processingData) {
// signals finished, so process captured data
controlLongGapTimer(false);
digitalWrite(ledPin, LOW);
logLine();
opMode == DOOYA_RX ? decodeDooya(signalIndex) : decodeUnknown(signalIndex);
memset(signalDurations, 0, BUF_SIZE * sizeof(uint32_t));
signalIndex = 0;
processingData = false;
}
else {
// capture received duration
static int currentLevel = LOW; // as sync starts with HIGH
if (longGapTimer) timerRestart(longGapTimer);
int nextLevel = digitalRead(rxPin); // logic level of signal that has just started
digitalWrite(ledPin, nextLevel); // visual indicator of signal level
// interrupt is on change, so completed signal is previous rxPin level
// store captured signal duration, and start next signal level duration
if (signalIndex == 0) controlLongGapTimer(true); // first signal, start msg timeout
if (storeSignalDuration(currentLevel, signalIndex)) signalIndex++;
if (signalIndex >= BUF_SIZE) processingData = true; // buffer full, reset for further sequence
currentLevel = nextLevel;
}
}
}
/***************** Transmit message *******************/
static void transmitBit(uint8_t bitIdx) {
// transmit a single bit
digitalWrite(txPin, HIGH);
digitalWrite(ledPin, HIGH);
delayMicroseconds(pulseDurations[0][bitIdx]);
digitalWrite(txPin, LOW);
digitalWrite(ledPin, LOW);
delayMicroseconds(pulseDurations[1][bitIdx]);
}
static void setBitDurations(uint8_t bitIdx, uint8_t bitVal) {
pulseDurations[0][bitIdx] = bitVal == '0' ? SHORT_SIGNAL : LONG_SIGNAL;
pulseDurations[1][bitIdx] = bitVal == '0' ? LONG_SIGNAL : SHORT_SIGNAL;
}
static void sendDooyaCmd(int command, uint8_t repeatCmd = 0) {
// construct bit stream comprising requested command, repeated for given amount
if (!repeatCmd) repeatCmd = DOOYA_RPT;
LOG_INF("%s: %s %s %s %s", cmdStr[command], deviceId, channelData, commandBits[command], trailingBits[command]);
// prep command stream
pulseDurations[0][0] = SYNC_HIGH;
pulseDurations[1][0] = SYNC_LOW;
uint8_t bitIdx = 1;
for (int i = 0; i < strlen(deviceId); i++) setBitDurations(bitIdx + i, deviceId[i]);
bitIdx += strlen(deviceId);
for (int i = 0; i < strlen(channelData); i++) setBitDurations(bitIdx + i, channelData[i]);
bitIdx += strlen(channelData);
for (int i = 0; i < strlen(commandBits[command]); i++) setBitDurations(bitIdx + i, commandBits[command][i]);
bitIdx += strlen(commandBits[command]);
// transmit command stream
while (repeatCmd--) {
for (int i = 0; i < bitIdx; i++) transmitBit(i);
delay(msgGap);
}
if (dbgVerbose) {
// display message data as derived pulse lengths
signalDurations[0] = msgGap * 1000;
for (int i = 0; i < bitIdx; i++) {
signalDurations[1 + (i * 2)] = pulseDurations[0][i];
signalDurations[1 + (i * 2) + 1] = pulseDurations[1][i];
}
displayDooyaData(0, (1 + bitIdx + 8) * 2); // fill out to 48 bits + sync
}
}
static void sendCustomCmd(int thisCommand) {
// user to provide code for each command:
// populate pulseDurations[2][number of bits + sync] with the high / low pulse durations per bit
// call transmitBit() for each bit in pulseDurations[][]
LOG_WRN("Need to provide sendCustomCmd() function");
}
static void transmitTask(void* parameter) {
// loops to service each transmit command
uint32_t currentPriority = 0;
while (true) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
currentPriority = cmdPriority;
// allow latest command to override current command [not relevant here]
if (currentPriority == cmdPriority) opMode == DOOYA_TX ? sendDooyaCmd(thisAction) : sendCustomCmd(thisAction);
}
vTaskDelete(NULL);
}
void sendUserCmd(int userAction) {
// user command received from browser web page
thisAction = userAction;
if (opMode >= DOOYA_TX && userAction) {
cmdPriority++;
xTaskNotifyGive(transmitTaskHandle);
}
}
/**************** init *******************/
// Dooya command ISRs called by pin interrupt
void IRAM_ATTR extendISR() {
cmdPriority++;
thisAction = EXTEND1;
wakeTask(transmitTaskHandle);
}
void IRAM_ATTR stopISR() {
cmdPriority++;
thisAction = STOP_ACTION;
wakeTask(transmitTaskHandle);
}
void IRAM_ATTR retractISR() {
cmdPriority++;
thisAction = RETRACT1;
wakeTask(transmitTaskHandle);
}
void IRAM_ATTR lightISR() {
cmdPriority++;
thisAction = TOGGLE_LED1;
wakeTask(transmitTaskHandle);
}
void IRAM_ATTR p2ISR() {
cmdPriority++;
thisAction = P2_COMMAND;
wakeTask(transmitTaskHandle);
}
void appSetup() {
pinMode(ledPin, OUTPUT);
if (opMode >= DOOYA_TX) {
// encode and transmit data
if (transmitTaskHandle == NULL) xTaskCreate(transmitTask, "transmitTask",2048, NULL, 1, &transmitTaskHandle);
pinMode(txPin, OUTPUT);
LOG_INF("Send %s commands", opMode == DOOYA_TX ? "Dooya" : "Custom");
// set up button pins if required
if (extendPin > 0) {
pinMode(extendPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(extendPin), extendISR, FALLING);
}
if (stopPin > 0) {
pinMode(stopPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(stopPin), stopISR, FALLING);
}
if (retractPin > 0) {
pinMode(retractPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(retractPin), retractISR, FALLING);
}
if (lightPin > 0) {
pinMode(lightPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(lightPin), lightISR, FALLING);
}
if (p2Pin > 0) {
pinMode(p2Pin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(p2Pin), p2ISR, FALLING);
}
} else {
// decode received data
if (receiveTaskHandle == NULL) xTaskCreate(receiveTask, "receiveTask", 4096, NULL, 2, &receiveTaskHandle);
pinMode(rxPin, INPUT);
attachInterrupt(rxPin, &handleInterrupt, CHANGE);
opMode == UNKNOWN_RX ? LOG_INF("Protocol sniffing") : LOG_INF("Dooya capture");
}
}