In Part One I covered the byte codes sent by the Zoom Remote Controller RC1 and decoded data sent over the wire to the remote from the Zoom H2n Recorder.
In this post I will be covering the use of an Arduino style micro-controller to decode the signals sent by the remote, then control the recorder. I have used a Tennsy 3.1 Arduino clone as this is a small controller with two additional hardware serial ports, works with 3.3volt logic, and a with the addition of a crystal and button battery a real-time clock.

Setup and Connections
The connections on the remotes four pin 2.5mm jack, with pin one being the tip:
- Remote Receive – RX
- Remote Transmit – TX
- Ground
- 3.1V – Power
On the Teensy there are two hardware serial UARTs available in addition to that used by the USB port, UART2: Pin 9 (RX2), Pin 10 (TX2) and UART3: Pin 7 (RX3), Pin 8 (TX3). Serial data is sent at 2400 baud, 8 bits, no parity, 1 stop (8n1). The response data shown is for when the recorder is in XY Stereo mode (0x20, 0x21), different codes are returned when other recording modes are used, see the end of Part One for details.
Serial Monitor
This first chunk of code is for monitoring the outputs of the remote control and recorder. Connect Remote Receive – RX on the remote to RX2 – Pin 9 on the Teensy and Remote Transmit – TX to RX3 – Pin 7 and Ground to Ground on the Teensy. This program will output data received to the Arduino IDE’s serial monitor.
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 |
//written for a Teensy 3.x #define MONITOR_LED 13 int incomingByte2 = 0; int incomingByte3 = 0; unsigned long nowMillis = 0; void printByte(int p, int b) { char out[40]; sprintf(out, "RX%d: %8lu\t0x%x\t%d",p,nowMillis,b,b); Serial.println(out); } void setup() { delay(1000); Serial.begin(9600); Serial.println("ready"); // incoming data Serial2.begin(2400, SERIAL_8N1); // Remote Receive - RX Serial3.begin(2400, SERIAL_8N1); // Remote Transmit - TX pinMode(MONITOR_LED, OUTPUT); digitalWrite(MONITOR_LED, HIGH); } void loop() { nowMillis = millis(); if (Serial2.available() > 0) { incomingByte2 = Serial2.read(); printByte(2, incomingByte2); } if (Serial3.available() > 0) { incomingByte3 = Serial3.read(); printByte(3, incomingByte3); } } |
The output is in four columns; the UART seeing activity, current milliseconds and the received data in hexadecimal and decimal values.
Taking Control – But Not Listening
Sending the command to the recorder blindly is quite straight forward, just send the bytes to the Remote Transmit – TX pin on the recorder (Zoom RX). This can be seen in the following, when run it starts the recorder recording for ten seconds. Connect: Remote TX to TX2 on the Teensy, Remote RX to RX2 and Ground to Ground.
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 |
//written for a Teensy 3.x #define MONITOR_LED 13 // bytes to start and stop the recorder, a negative number is used as a delay int record[5] = { 0x81, 0x0, -100, 0x80, 0x0 }; void zoomTX(int d[], int len) { // show what is going to be done for (int i = 0; i < len; i++) { char out[40]; if (d[i] < 0) { sprintf(out,"TX2: delay(%d)",abs(d[i])); Serial.println(out); } else { sprintf(out, "TX2: %d\t0x%x", i, d[i]); Serial.println(out); } } // do the command for (int i = 0; i < len; i++) { if (d[i] < 0) { delay(abs(d[i])); } else { Serial2.write(d[i]); } } } void setup() { delay(1000); Serial.begin(9600); Serial.println("ready"); Serial2.begin(2400, SERIAL_8N1); // connection to Zoom. pinMode(MONITOR_LED, OUTPUT); digitalWrite(MONITOR_LED, LOW); delay(2000); Serial.println("REC: start"); zoomTX(record, sizeof(record)/sizeof(int)); digitalWrite(MONITOR_LED, HIGH); Serial.println("-----------------"); delay(10000); // wait ten seconds Serial.println("REC: stop"); zoomTX(record, sizeof(record)/sizeof(int)); digitalWrite(MONITOR_LED, LOW); Serial.println("-end-"); } void loop() { // nothing to loop } |
Taking Control – And Listening for a Reply
The next stage is to have the Teensy control the Zoom and listen for a response from the recorder. Again, as before connect: Remote TX to TX2 on the Teensy, Remote RX to RX2 and Ground to Ground. For the demonstration I have added three buttons to act as the controller.

The following code needs more development work, I ran out of time, but I think gives a good starting point for further investigation. I have placed the commands for the remote in a structure, each command; record, pause and mark has four components, the command to transmit to the Zoom, the expected responses when starting and stopping and a flag to store the status.
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 |
//written for a Teensy 3.x #define MONITOR_LED 13 #define RED_BTN 2 #define GRN_BTN 3 #define BLU_BTN 4 unsigned long nowMillis = 0; unsigned long prevMillis = 0; unsigned long buttonPressDelay = 400; unsigned long serialTimeout = 8000; // it can take a few seconds for the zoom to start recording struct ZOOMTX { int record[4][5] = { {0x81, 0x0, -100, 0x80, 0x0}, // Transmit command data - a minus number is used as a delay(xxx) {0x20, 0x20, 0x21}, // Expected Response - Start (in XY Stereo mode) {0x21, 0x21, 0x20}, // Response - Stop {0} }; // status: record[3][0] = 1: recording, 0: not recording int pause[4][5] = { {0x80, 0x2, -100, 0x80, 0x0}, {0x21, 0x21, 0x20}, {0x20, 0x21}, {0} }; int mark[4][5] = { {0x80, 0x1, -100, 0x80, 0x0}, {0x20, 0x20}, {0x20, 0x20}, {0} }; int noAction[2] = { 0x20, 0x20 }; // this happens when pause is pressed while not recording. Response in XY Stereo mode }; struct LED_MONITOR { const unsigned int onTime = 300; const unsigned int offTime = 500; unsigned long prevMillis = 0; unsigned int wait = onTime; boolean state = true; }; ZOOMTX ZoomTX; LED_MONITOR LEDmonitor; void printByte(int p, int b) { char out[40]; sprintf(out, "RX%d: %8lu\t0x%x\t%d", p, millis(), b, b); Serial.println(out); } boolean getButton(byte btn) { boolean b = digitalRead(btn); if (b == true) { while (digitalRead(btn) != b) { delay(5); // wait until button release; } return true; } return false; } void zoomTransmit(int d[], int len) { for (int i = 0; i < len; i++) { char out[40]; if (d[i] < 0) { sprintf(out, "TX2: delay(%d)", abs(d[i])); Serial.println(out); } else { sprintf(out, "TX2: %d\t0x%x", i, d[i]); Serial.println(out); } } for (int i = 0; i < len; i++) { if (d[i] < 0) { delay(abs(d[i])); } else { Serial2.write(d[i]); } } } // http://forum.arduino.cc/index.php?topic=5157.0 boolean arrayCompare(int *a, int *b, int len_a, int len_b) { int n; if (len_a != len_b) { return false; } for (n = 0; n < len_a; n++) { if (a[n] != b[n]) { return false; } } return true; } int arrayLen(int a[5]) { int c=0; for (int n = 0; n < 5; n++) { if (a[n] == 0) { break; } c++; } return c; } boolean zoomCommand(int cmd[4][5]) { int responseBytes[5] = {0}; // for incoming serial data unsigned long serialNow = millis(); int expectedResponse[3] = {0}; int responseLen = 0; if (cmd[3][0] == 0) { // copy the expected response into expectedResponse Serial.println("start"); memcpy(expectedResponse, cmd[1], sizeof(cmd[1])); // expected start response responseLen = arrayLen(cmd[1]); } else { Serial.println("stop"); memcpy(expectedResponse, cmd[2], sizeof(cmd[2])); // expected stop response responseLen = arrayLen(cmd[2]); } // clear the incoming serial2 buffer while(Serial2.available()) { Serial2.read(); } // send the command to the zoom zoomTransmit(cmd[0], sizeof(cmd[0]) / sizeof(int)); // listen for a response while ((Serial2.available() > 0 && Serial2.available() < responseLen) && ((millis() - serialNow) < serialTimeout)) { } // listen for responseLen bytes or until serialTimeout if (Serial2.available() == 2) { // only two bytes received? for (int n = 0; n < 2; n++) { responseBytes[n] = Serial2.read(); printByte(2, responseBytes[n]); } if (arrayCompare(responseBytes, expectedResponse, 2, 2)) { Serial.println("RX: OK - Acknowledged! (two bytes)"); // mark has a two byte response return true; } if (arrayCompare(responseBytes, ZoomTX.noAction, 2, 2)) { Serial.println("RX: no action data received - Zoom not in the state we thought it was?"); } else { Serial.println("RX: did not understand the response - two bytes"); // probably not in XY Stereo mode? } return false; } else { // three or more bytes for (int n = 0; n < 3; n++) { // check the first three responseBytes[n] = Serial2.read(); printByte(2, responseBytes[n]); } if (arrayCompare(responseBytes, expectedResponse, 3, 3) == true) { Serial.println("RX: OK - Acknowledged!"); return true; } else { Serial.println("RX: did not understand the response - three bytes"); // probably not in XY Stereo mode? } } return false; } // https://www.baldengineer.com/millis-ind-on-off-times.html void toggleMoitorLEDstate(unsigned long n) { if ((unsigned long)(n - LEDmonitor.prevMillis) >= LEDmonitor.wait) { if (LEDmonitor.state) { LEDmonitor.wait = LEDmonitor.offTime; } else { LEDmonitor.wait = LEDmonitor.onTime; } LEDmonitor.state = !(LEDmonitor.state); LEDmonitor.prevMillis = n; } } void setup() { delay(1000); Serial.begin(9600); Serial.println("ready"); Serial2.begin(2400, SERIAL_8N1); // zoom connection pinMode(MONITOR_LED, OUTPUT); pinMode(RED_BTN, INPUT); pinMode(GRN_BTN, INPUT); pinMode(BLU_BTN, INPUT); digitalWrite(MONITOR_LED, LOW); } void loop() { nowMillis = millis(); if (getButton(RED_BTN) && nowMillis >= (prevMillis + buttonPressDelay)) { Serial.print("RECORD: "); if (zoomCommand(ZoomTX.record)) { if (ZoomTX.record[3][0] == 0) { digitalWrite(MONITOR_LED, HIGH); ZoomTX.record[3][0] = 1; // recording started } else { digitalWrite(MONITOR_LED, LOW); ZoomTX.record[3][0] = 0; // recording stopped ZoomTX.pause[3][0] = 0; // recording now not paused (just in case) } } else { Serial.println("RECORD COMMAND FAILED!"); } prevMillis = nowMillis; } // pause only works when recording if (getButton(GRN_BTN) && ZoomTX.record[3][0] == 1 && nowMillis >= (prevMillis + buttonPressDelay)) { Serial.print("PAUSE: "); if (zoomCommand(ZoomTX.pause)) { ZoomTX.pause[3][0] = !(ZoomTX.pause[3][0]); // toggle the pause status digitalWrite(MONITOR_LED, HIGH); } else { Serial.println("PAUSE COMMAND FAILED!"); // probably not recording, or not recording in XY Stereo } prevMillis = nowMillis; } if (getButton(BLU_BTN) && ZoomTX.record[3][0] == 1 && nowMillis >= (prevMillis + buttonPressDelay)) { Serial.print("MARK: "); if (zoomCommand(ZoomTX.mark)) { Serial.println("recording marked"); } else { Serial.println("PAUSE COMMAND FAILED!"); // probably not recording, or not recording in XY Stereo } prevMillis = nowMillis; } // blink the LED while paused if (ZoomTX.pause[3][0] == 1) { digitalWrite(MONITOR_LED, LEDmonitor.state); toggleMoitorLEDstate(nowMillis); } } |
There is a problem when resuming from pause, because the Zoom sends codes to flash the LED on the remote this can pick up the wrong pair of bytes; such as 0x21 0x21 instead of the expected 0x20 0x21.
I expect to be revisiting this, adding a timer function plus external battery for long running. I’m not sure how useful listening for a response is, sending the record command toggle on its own seems fairly robust without the need to check.