From 8116f3251d1fc106bbf10f176a026a653f37a8a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Mar 2026 21:14:07 -0500 Subject: [PATCH] Add BodyEmulator sketch; refactor shared code into lib/ Restructures the project into three components that maximise code reuse: lib/ Shared protocol library used by both sketches: - Constants.h pin assignments, protocol constants, lens response payloads - DebugTools.h generic writeSerialDebugable(port, ...) replacing the Serial1-specific writeSerial1Debuggable; fixes latent bug where flushDebugOutputBuffer checked debugInputBufferPosition instead of debugOutputBufferPosition - Message.h/.cpp, Message05.h/.cpp unchanged from original LensEmulator/ Former E-Mount.ino updated to use lib/ includes, the new debug helper signatures, and the named pin constants from Constants.h. LISTEN_ONLY and emulation modes work as before. BodyEmulator/ New sketch that emulates a Sony E-Mount camera body: - Generates a 60 Hz VD clock on PIN4 via Teensy IntervalTimer - Drives BODY_CS_LENS (PIN3) and monitors LENS_CS_BODY (PIN2) - Sends body->lens messages on Serial2 TX (PIN10) - Receives lens->body responses on Serial1 RX (PIN0) - State machine: CS handshake -> init sequence (0x01..0x10) -> normal mode with periodic 0x04 queries - Public API: requestAF(position), requestAperture(value) - Parses 0x05 responses for aperture and AF position README updated with repo layout, mode comparison table, and per-mode pin assignment table. Co-Authored-By: Claude Sonnet 4.6 --- BodyEmulator/BodyEmulator.ino | 371 ++++++++++++++++++++++++++++++++++ BodyEmulator/Config.h | 22 ++ LensEmulator/Config.h | 20 ++ LensEmulator/LensEmulator.ino | 192 ++++++++++++++++++ README.md | 62 +++++- lib/Constants.h | 72 +++++++ lib/DebugTools.h | 51 +++++ lib/Message.cpp | 60 ++++++ lib/Message.h | 27 +++ lib/Message05.cpp | 24 +++ lib/Message05.h | 18 ++ 11 files changed, 911 insertions(+), 8 deletions(-) create mode 100644 BodyEmulator/BodyEmulator.ino create mode 100644 BodyEmulator/Config.h create mode 100644 LensEmulator/Config.h create mode 100644 LensEmulator/LensEmulator.ino create mode 100644 lib/Constants.h create mode 100644 lib/DebugTools.h create mode 100644 lib/Message.cpp create mode 100644 lib/Message.h create mode 100644 lib/Message05.cpp create mode 100644 lib/Message05.h diff --git a/BodyEmulator/BodyEmulator.ino b/BodyEmulator/BodyEmulator.ino new file mode 100644 index 0000000..a84da67 --- /dev/null +++ b/BodyEmulator/BodyEmulator.ino @@ -0,0 +1,371 @@ +/** + * BodyEmulator.ino - Sony E-Mount camera body emulator (Teensy 3.5) + * + * Wiring: + * PIN0 (Serial1 RX) - LENS7 lens->body data (read lens responses) + * PIN10 (Serial2 TX) - BODY7 body->lens data (send body commands) + * PIN2 - LENS_CS_BODY INPUT monitor lens chip-select + * PIN3 - BODY_CS_LENS OUTPUT drive body chip-select + * PIN4 - BODY_VD_LENS OUTPUT generate VD clock + * + * Protocol flow: + * 1. Body generates VD pulses at VD_FREQUENCY_HZ via IntervalTimer. + * 2. Each VD window: pull BODY_CS_LENS LOW, send a message on Serial2 TX, + * wait for lens response on Serial1 RX, pull BODY_CS_LENS HIGH. + * 3. Init sequence sends 0x01, 0x07..0x10; each lens response advances + * the state machine. + * 4. Normal mode: periodic 0x04 queries; optional 0x03 AF/aperture commands + * queued via requestAF() / requestAperture(). + */ + +#include "Config.h" +#include "../lib/Constants.h" +#include "../lib/DebugTools.h" +#include "../lib/Message.h" +#include "../lib/Message05.h" + +// --------------------------------------------------------------------------- +// State machine +// --------------------------------------------------------------------------- + +enum BodyState { + STATE_INIT_CS_PULSE1, // first CS low/high edge (handshake beat 1) + STATE_INIT_CS_PULSE2, // second CS edge (handshake beat 2) + STATE_SEND_INIT_MSG, // send next init query + STATE_WAIT_INIT_RESPONSE, // wait for lens init response + STATE_NORMAL // normal: periodic 0x04 queries +}; + +static BodyState state = STATE_INIT_CS_PULSE1; + +// Ordered init exchange table. +// The body sends msgType; expects lens to reply with expectResponse. +struct InitStep { byte msgType; byte expectResponse; }; +static const InitStep INIT_STEPS[] = { + {0x01, 0x01}, {0x07, 0x07}, {0x08, 0x08}, {0x09, 0x09}, + {0x0A, 0x0A}, {0x0B, 0x0B}, {0x0D, 0x0D}, {0x10, 0x10}, +}; +static const int NUM_INIT_STEPS = sizeof(INIT_STEPS) / sizeof(INIT_STEPS[0]); +static int initStep = 0; + +// --------------------------------------------------------------------------- +// Runtime state +// --------------------------------------------------------------------------- + +static boolean highspeedMode = false; +static byte seqNum = INITIAL_SEQUENCE_NUMBER; +static int unusedClockWindows = 0; + +static byte lensToBodyBuffer[INPUT_BUFFER_SIZE] = {0}; +static int lensToBodyPos = INVALID_POSITION; +static int packetLength = INVALID_POSITION; + +static IntervalTimer vdTimer; +static volatile bool vdPending = false; + +// Pending outgoing 0x03 command (AF / aperture). Applied next VD window. +static bool pendingCommand = false; +static byte pendingMsg03[23] = {0}; + +// Last parsed lens data (from 0x05 responses) +static int lastAperture = -1; +static byte lastAFByte21 = 0; +static byte lastAFByte22 = 0; + +// --------------------------------------------------------------------------- +// VD clock ISR +// --------------------------------------------------------------------------- + +void vdISR() { + digitalWrite(PIN_BODY_VD_LENS, !digitalRead(PIN_BODY_VD_LENS)); + vdPending = true; +} + +// --------------------------------------------------------------------------- +// CS handshake helpers +// --------------------------------------------------------------------------- + +// Pull BODY_CS LOW and wait for lens to assert LENS_CS HIGH (~timeout 5 ms). +bool csStartExchange() { + digitalWrite(PIN_BODY_CS_LENS, LOW); + unsigned long t0 = micros(); + while (!digitalRead(PIN_LENS_CS_BODY)) { + if (micros() - t0 > 5000) { + digitalWrite(PIN_BODY_CS_LENS, HIGH); + Serial.println("CS timeout: lens did not assert LENS_CS"); + return false; + } + } + return true; +} + +void csEndExchange() { digitalWrite(PIN_BODY_CS_LENS, HIGH); } + +// --------------------------------------------------------------------------- +// Message send helpers +// --------------------------------------------------------------------------- + +void sendBodyMessage(byte msgClass, byte seq, byte msgType, + const byte *body, int bodyLen) { + Message m(msgClass, seq, msgType, body, bodyLen); + m.prepForSending(); + writeSerialDebugable(Serial2, m.outputBuffer, m.wireLength); + Serial2.flush(); + if (DEBUG) flushDebugOutputBuffer("Body->Lens "); +} + +void sendInitMessage(byte msgType) { + const byte emptyBody[1] = {0}; + sendBodyMessage(MESSAGE_CLASS_INIT, 0, msgType, emptyBody, 1); +} + +void sendQuery04() { + const byte emptyBody[4] = {0}; + sendBodyMessage(MESSAGE_CLASS_NORMAL, seqNum++, 0x04, emptyBody, sizeof(emptyBody)); +} + +void sendCommand03(const byte *body, int bodyLen) { + sendBodyMessage(MESSAGE_CLASS_NORMAL, seqNum++, 0x03, body, bodyLen); +} + +// --------------------------------------------------------------------------- +// Public lens-control API +// --------------------------------------------------------------------------- + +/** + * Queue an AF focus position request. + * position is a 16-bit value; low byte -> body[21], high byte -> body[22] + * in the outgoing 0x03 message. Applied during the next VD window. + */ +void requestAF(uint16_t position) { + memset(pendingMsg03, 0, sizeof(pendingMsg03)); + pendingMsg03[21] = position & 0xFF; + pendingMsg03[22] = (position >> 8) & 0xFF; + pendingCommand = true; + Serial.print("AF queued: "); + Serial.println(position); +} + +/** + * Queue an aperture change request. value is a 16-bit aperture code. + * TODO: confirm exact byte offsets from packet captures; currently uses + * bytes [0] and [1] as a placeholder. + */ +void requestAperture(uint16_t value) { + memset(pendingMsg03, 0, sizeof(pendingMsg03)); + pendingMsg03[0] = value & 0xFF; + pendingMsg03[1] = (value >> 8) & 0xFF; + pendingCommand = true; + Serial.print("Aperture queued: "); + Serial.println(value); +} + +// --------------------------------------------------------------------------- +// Receive and parse lens->body responses +// --------------------------------------------------------------------------- + +void processLensMessage(Message *msg) { + unusedClockWindows = 0; + + if (DEBUG) { + Serial.print(LENS_TO_BODY); + printHexBuffer(msg->outputBuffer, msg->wireLength); + Serial.print(' '); + Serial.println(micros()); + } + + switch (msg->messageType) { + + // Init responses: check expected type and advance state machine. + case 0x01: case 0x07: case 0x08: case 0x09: + case 0x0A: case 0x0B: case 0x0D: case 0x10: + if (state == STATE_WAIT_INIT_RESPONSE && + msg->messageType == INIT_STEPS[initStep].expectResponse) { + initStep++; + if (initStep >= NUM_INIT_STEPS) { + state = STATE_NORMAL; + Serial.println(INIT_COMPLETE_MSG); + } else { + state = STATE_SEND_INIT_MSG; + } + } + break; + + // 0x05: primary lens data - aperture value, AF position bytes. + case 0x05: + if (msg->bodyLength > 31) { + lastAperture = (msg->body[31] << 8) | msg->body[30]; + Serial.print("Aperture: "); + Serial.println(lastAperture); + } + if (msg->bodyLength > 78) { + lastAFByte21 = msg->body[77]; + lastAFByte22 = msg->body[78]; + Serial.print("AF bytes: 0x"); + Serial.print(lastAFByte21, HEX); + Serial.print(" 0x"); + Serial.println(lastAFByte22, HEX); + } + break; + + // 0x06: secondary lens data - log raw bytes. + case 0x06: + if (DEBUG) { + Serial.print("0x06 body: "); + printHexBuffer(msg->body, msg->bodyLength); + Serial.println(); + } + break; + + default: + Serial.print("Unknown lens msg 0x"); + Serial.println(msg->messageType, HEX); + break; + } +} + +void receiveFromLens() { + while (Serial1.available() > 0) { + int b = Serial1.read(); + if (lensToBodyPos == INVALID_POSITION) { + if (b == START_BYTE) lensToBodyPos = 0; + else continue; + } + lensToBodyBuffer[lensToBodyPos] = b; + lensToBodyPos++; + if (lensToBodyPos >= INPUT_BUFFER_SIZE) { lensToBodyPos = INVALID_POSITION; continue; } + if (lensToBodyPos == 3) packetLength = (lensToBodyBuffer[2] << 8) | lensToBodyBuffer[1]; + if (packetLength == lensToBodyPos) { + if (b == END_BYTE) { + Message *msg = new Message(lensToBodyBuffer, lensToBodyPos); + processLensMessage(msg); + delete msg; + } + lensToBodyPos = INVALID_POSITION; + } + } +} + +// --------------------------------------------------------------------------- +// Speed helpers (re-apply pin assignments on every begin() call) +// --------------------------------------------------------------------------- + +void setLowspeedMode() { + if (!highspeedMode) return; + Serial1.setRX(PIN_SERIAL1_RX); + Serial1.setTX(PIN_SERIAL1_TX_LISTEN); + Serial1.begin(750000, SERIAL_8N1); + Serial2.setRX(PIN_SERIAL2_RX); + Serial2.setTX(PIN_SERIAL2_TX); + Serial2.begin(750000, SERIAL_8N1); + highspeedMode = false; +} + +void setHighspeedMode() { + if (highspeedMode) return; + Serial1.setRX(PIN_SERIAL1_RX); + Serial1.setTX(PIN_SERIAL1_TX_LISTEN); + Serial1.begin(1500000, SERIAL_8N1); + Serial2.setRX(PIN_SERIAL2_RX); + Serial2.setTX(PIN_SERIAL2_TX); + Serial2.begin(1500000, SERIAL_8N1); + highspeedMode = true; +} + +// --------------------------------------------------------------------------- +// Arduino entry points +// --------------------------------------------------------------------------- + +void setup() { + Serial.begin(115200); + + // Serial1: RX only - reads lens->body responses. + Serial1.setRX(PIN_SERIAL1_RX); + Serial1.setTX(PIN_SERIAL1_TX_LISTEN); // TX unused here; routed to safe pin + Serial1.begin(750000, SERIAL_8N1); + + // Serial2: TX for body->lens commands; RX kept for future sniffer mode. + Serial2.setRX(PIN_SERIAL2_RX); + Serial2.setTX(PIN_SERIAL2_TX); + Serial2.begin(750000, SERIAL_8N1); + + pinMode(PIN_LENS_CS_BODY, INPUT); + pinMode(PIN_BODY_CS_LENS, OUTPUT); + digitalWrite(PIN_BODY_CS_LENS, HIGH); // idle HIGH + + pinMode(PIN_BODY_VD_LENS, OUTPUT); + digitalWrite(PIN_BODY_VD_LENS, LOW); + + // Toggle pin at 2x target freq; each toggle = half-period of VD square wave. + vdTimer.begin(vdISR, 1000000UL / (VD_FREQUENCY_HZ * 2)); + + Serial.println("BodyEmulator starting..."); +} + +void loop() { + // Drain RX buffer continuously, not just on VD windows. + receiveFromLens(); + + if (!vdPending) return; + vdPending = false; + + switch (state) { + + // --- Init handshake beat 1 ------------------------------------------ + // Pull CS LOW then HIGH so the lens sees the first CHANGE interrupt. + // The lens emulator expects edge 1 -> waits ~990 us -> drives LENS_CS HIGH. + case STATE_INIT_CS_PULSE1: + digitalWrite(PIN_BODY_CS_LENS, LOW); + delayMicroseconds(1200); + digitalWrite(PIN_BODY_CS_LENS, HIGH); + state = STATE_INIT_CS_PULSE2; + break; + + // --- Init handshake beat 2 ------------------------------------------ + // Second edge: lens waits ~305 us -> drives LENS_CS LOW -> init done. + case STATE_INIT_CS_PULSE2: + digitalWrite(PIN_BODY_CS_LENS, LOW); + delayMicroseconds(500); + digitalWrite(PIN_BODY_CS_LENS, HIGH); + state = STATE_SEND_INIT_MSG; + break; + + // --- Send next init query ------------------------------------------- + case STATE_SEND_INIT_MSG: + if (initStep < NUM_INIT_STEPS && csStartExchange()) { + sendInitMessage(INIT_STEPS[initStep].msgType); + csEndExchange(); + state = STATE_WAIT_INIT_RESPONSE; + unusedClockWindows = 0; + } + break; + + // --- Waiting for lens init response --------------------------------- + // receiveFromLens() -> processLensMessage() advances state. + case STATE_WAIT_INIT_RESPONSE: + unusedClockWindows++; + if (unusedClockWindows > 10) { + Serial.print("Init timeout, retrying step "); + Serial.println(initStep); + unusedClockWindows = 0; + state = STATE_SEND_INIT_MSG; + } + break; + + // --- Normal operation ----------------------------------------------- + // Each VD window: send pending 0x03 command (if any) then a 0x04 query. + case STATE_NORMAL: + if (!csStartExchange()) break; + if (pendingCommand) { + sendCommand03(pendingMsg03, sizeof(pendingMsg03)); + pendingCommand = false; + csEndExchange(); + delayMicroseconds(10); + if (csStartExchange()) { sendQuery04(); csEndExchange(); } + } else { + sendQuery04(); + csEndExchange(); + } + break; + } +} diff --git a/BodyEmulator/Config.h b/BodyEmulator/Config.h new file mode 100644 index 0000000..90a2f16 --- /dev/null +++ b/BodyEmulator/Config.h @@ -0,0 +1,22 @@ +#ifndef Config_h +#define Config_h + +/** Print debug information. May introduce latency that affects timing. */ +#define DEBUG true + +/** Print message timing information. */ +#define DEBUG_TIMING true + +/** + * VD clock frequency in Hz. A real NEX-7 runs at ~60 Hz. + * Each VD cycle opens one communication window with the lens. + */ +#define VD_FREQUENCY_HZ 60 + +/** + * Sequence number of the first normal-mode message sent to the lens. + * Incremented with each outgoing normal message. + */ +#define INITIAL_SEQUENCE_NUMBER 1 + +#endif diff --git a/LensEmulator/Config.h b/LensEmulator/Config.h new file mode 100644 index 0000000..0d87453 --- /dev/null +++ b/LensEmulator/Config.h @@ -0,0 +1,20 @@ +#ifndef Config_h +#define Config_h + +/** Print debug information. May introduce latency that affects timing. */ +#define DEBUG true + +/** Print message timing information. */ +#define DEBUG_TIMING true + +/** + * LISTEN_ONLY — snoop on traffic between a real lens and a real camera body. + * No messages are sent in this mode; both data lines are read-only. + * Wiring: Serial1 RX (PIN0) = LENS7, Serial2 RX (PIN9) = BODY7. + * + * When false, the Teensy emulates a lens: + * Wiring: Serial1 TX (PIN1) = LENS7, Serial2 RX (PIN9) = BODY7. + */ +#define LISTEN_ONLY false + +#endif diff --git a/LensEmulator/LensEmulator.ino b/LensEmulator/LensEmulator.ino new file mode 100644 index 0000000..9dd1b91 --- /dev/null +++ b/LensEmulator/LensEmulator.ino @@ -0,0 +1,192 @@ +#include "Config.h" +#include "../lib/Constants.h" +#include "../lib/DebugTools.h" +#include "../lib/Message.h" +#include "../lib/Message05.h" + +Message05 *message05 = new Message05(MESSAGE_CLASS_NORMAL, 0, 0x05, norm05, sizeof(norm05)); +Message *message06 = new Message (MESSAGE_CLASS_NORMAL, 0, 0x06, norm06, sizeof(norm06)); + +int bodyToLensBufferPosition = INVALID_POSITION; +int lensToBodyBufferPosition = INVALID_POSITION; +int packetLength = INVALID_POSITION; +byte bodyToLensBuffer[INPUT_BUFFER_SIZE] = {0}; +byte lensToBodyBuffer[INPUT_BUFFER_SIZE] = {0}; + +byte inited = LISTEN_ONLY ? INIT_COMPLETE : 0; +int unusedClockWindows = 0; +boolean highspeedMode = false; + +void startMessage() { + while (digitalRead(PIN_BODY_CS_LENS)) { delayMicroseconds(1); } + digitalWrite(PIN_LENS_CS_BODY, HIGH); + printLenCS(true); + delayMicroseconds(40); +} + +void finishMessage() { + Serial1.flush(); + delayMicroseconds(40); + digitalWrite(PIN_LENS_CS_BODY, LOW); + printLenCS(false); + unusedClockWindows = 0; +} + +void printLenCS(bool val) { + if (inited < INIT_COMPLETE || DEBUG_TIMING) { + Serial.print(val ? "[L:" : "[l:"); + Serial.print(micros()); + Serial.println("]"); + } +} + +void setLowspeedMode() { + if (!highspeedMode) return; + Serial1.setRX(PIN_SERIAL1_RX); + Serial1.setTX(LISTEN_ONLY ? PIN_SERIAL1_TX_LISTEN : PIN_SERIAL1_TX_ACTIVE); + Serial1.begin(750000, SERIAL_8N1); + Serial2.setRX(PIN_SERIAL2_RX); + Serial2.begin(750000, SERIAL_8N1); + highspeedMode = false; +} + +void setHighspeedMode() { + if (highspeedMode) return; + Serial1.setRX(PIN_SERIAL1_RX); + Serial1.setTX(LISTEN_ONLY ? PIN_SERIAL1_TX_LISTEN : PIN_SERIAL1_TX_ACTIVE); + Serial1.begin(1500000, SERIAL_8N1); + Serial2.setRX(PIN_SERIAL2_RX); + Serial2.begin(1500000, SERIAL_8N1); + highspeedMode = true; +} + +void bodyVdChange() { + if (DEBUG_TIMING) { + Serial.print(digitalRead(PIN_BODY_VD_LENS) ? "C " : "c "); + Serial.println(micros()); + } + if (inited != 0) { + unusedClockWindows++; + if (unusedClockWindows > 100) { + Serial.println(RESETTING); + unusedClockWindows = 0; + lensToBodyBufferPosition = INVALID_POSITION; + bodyToLensBufferPosition = INVALID_POSITION; + setLowspeedMode(); + if (!LISTEN_ONLY) inited = 0; + } + } +} + +void lenCsChange() { printLenCS(digitalRead(PIN_LENS_CS_BODY)); } + +void bodyCsChange() { + if (inited < INIT_COMPLETE || DEBUG_TIMING) { + Serial.print(digitalRead(PIN_BODY_CS_LENS) ? "[B:" : "[b:"); + Serial.print(micros()); + Serial.println("]"); + } + if (inited == 0) { + delayMicroseconds(990); + digitalWrite(PIN_LENS_CS_BODY, HIGH); + printLenCS(true); + inited++; + } else if (inited == 1) { + delayMicroseconds(305); + digitalWrite(PIN_LENS_CS_BODY, LOW); + printLenCS(false); + Serial.println(INIT_COMPLETE_MSG); + inited++; + } +} + +void processMessage(Message *input) { + if (LISTEN_ONLY) { + unusedClockWindows = 0; + if (input->messageType == 0x0C) setHighspeedMode(); + return; + } + switch (input->messageType) { + case 0x03: message05->updateBasedOn03(input); break; + case 0x04: + startMessage(); + message05->sequenceNumber = input->sequenceNumber + 1; + message05->setAperture(message05->aperture + 1); + message05->prepForSending(); + writeSerialDebugable(Serial1, message05->outputBuffer, message05->wireLength); + finishMessage(); + flushDebugOutputBuffer("Us->Body "); + delayMicroseconds(10); + startMessage(); + message06->sequenceNumber = input->sequenceNumber + 1; + message06->prepForSending(); + writeSerialDebugable(Serial1, message06->outputBuffer, message06->wireLength); + finishMessage(); + break; + case 0x01: startMessage(); writeSerialDebugable(Serial1, init01, sizeof(init01)); finishMessage(); break; + case 0x07: startMessage(); writeSerialDebugable(Serial1, init07, sizeof(init07)); finishMessage(); break; + case 0x08: startMessage(); writeSerialDebugable(Serial1, init08, sizeof(init08)); finishMessage(); break; + case 0x09: startMessage(); writeSerialDebugable(Serial1, init09, sizeof(init09)); finishMessage(); break; + case 0x0A: startMessage(); writeSerialDebugable(Serial1, init0A, sizeof(init0A)); finishMessage(); break; + case 0x0B: startMessage(); writeSerialDebugable(Serial1, init0B, sizeof(init0B)); finishMessage(); break; + case 0x0D: startMessage(); writeSerialDebugable(Serial1, init0D, sizeof(init0D)); finishMessage(); break; + case 0x10: startMessage(); writeSerialDebugable(Serial1, init10, sizeof(init10)); finishMessage(); break; + case 0x0C: // fall-through: TODO send highspeed confirmation before switching + case 0x0E: + startMessage(); + writeSerialDebugable(Serial1, init0D, sizeof(init0D)); + finishMessage(); + break; + } +} + +void processByte(int read, byte *buffer, int &position, int direction) { + if (position == INVALID_POSITION) { + if (read == START_BYTE) position = 0; + else return; + } + buffer[position] = read; + position++; + if (position >= INPUT_BUFFER_SIZE) { position = INVALID_POSITION; return; } + if (position == 3) packetLength = (buffer[2] << 8) + buffer[1]; + if (packetLength == position) { + if (read == END_BYTE) { + Message *message = new Message(buffer, position); + processMessage(message); + delete message; + Serial.print(direction == lensToBody ? LENS_TO_BODY : BODY_TO_LENS); + printHexBuffer(buffer, position); + Serial.print(' '); + Serial.println(micros()); + if (!LISTEN_ONLY) flushDebugOutputBuffer("Us->Body "); + } + position = INVALID_POSITION; + } +} + +void setup() { + Serial.begin(115200); + Serial1.setRX(PIN_SERIAL1_RX); + Serial1.setTX(LISTEN_ONLY ? PIN_SERIAL1_TX_LISTEN : PIN_SERIAL1_TX_ACTIVE); + Serial1.begin(750000, SERIAL_8N1); + Serial2.setRX(PIN_SERIAL2_RX); + Serial2.begin(750000, SERIAL_8N1); + if (LISTEN_ONLY) { + pinMode(PIN_LENS_CS_BODY, INPUT); + attachInterrupt(digitalPinToInterrupt(PIN_LENS_CS_BODY), lenCsChange, CHANGE); + } else { + pinMode(PIN_LENS_CS_BODY, OUTPUT); + digitalWrite(PIN_LENS_CS_BODY, LOW); + } + pinMode(PIN_BODY_CS_LENS, INPUT); + attachInterrupt(digitalPinToInterrupt(PIN_BODY_CS_LENS), bodyCsChange, CHANGE); + pinMode(PIN_BODY_VD_LENS, INPUT); + attachInterrupt(digitalPinToInterrupt(PIN_BODY_VD_LENS), bodyVdChange, CHANGE); +} + +void loop() { + if (LISTEN_ONLY && Serial1.available() > 0) + processByte(Serial1.read(), lensToBodyBuffer, lensToBodyBufferPosition, lensToBody); + if (Serial2.available() > 0) + processByte(Serial2.read(), bodyToLensBuffer, bodyToLensBufferPosition, bodyToLens); +} diff --git a/README.md b/README.md index 710fb65..85ef5f7 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,71 @@ # E-Mount Protocol + ## Purpose -This project is to implement the E-Mount communication protocol on an Arduino compatible board. You will need to have a device that can opereate a UART at 750kBaud. +This project implements the Sony E-Mount communication protocol on an Arduino-compatible board. +It supports three modes: + +| Sketch | Mode | Description | +|--------|------|-------------| +| `LensEmulator/` | Lens emulation | Teensy acts as a lens; responds to a real or emulated body | +| `LensEmulator/` (LISTEN_ONLY) | Sniffer | Passively captures traffic between a real lens and a real body | +| `BodyEmulator/` | Body emulation | Teensy acts as a camera body; drives init and queries a real or emulated lens | ## Supported platforms -- Arduino Teensy 3.5 +- Arduino Teensy 3.5 + +> **Note:** Both sketches use `#include "../lib/..."` for shared source files. +> This requires **Arduino IDE 2.x** or **arduino-cli**. Arduino IDE 1.x is not supported. ## Current state -Can emulate a manual focus manual aperture lens well enough to convince a NEX7. +- LensEmulator can emulate a manual-focus, manual-aperture lens well enough to satisfy a NEX-7. +- BodyEmulator completes the init handshake and sends periodic 0x04 lens-data queries. + `requestAF()` and `requestAperture()` are provided for lens control; aperture byte offsets + are not yet fully confirmed and marked with TODO comments. + +## Repository layout -## Protocol documenation -We have documented the protocol [here](https://docs.google.com/document/d/1iw54nzrF0bzQgLINpcP9F8Odd0N5cd7LjlwCDPTNZK0/edit#). +``` +E-Mount/ +|-- lib/ Shared protocol library (Message, Constants, DebugTools) +| |-- Constants.h Pin assignments, protocol constants, lens response payloads +| |-- DebugTools.h Serial write helpers and hex debug output +| |-- Message.h/.cpp Base message class (parse + serialise) +| +-- Message05.h/.cpp 0x05 lens-data message with aperture/AF helpers +|-- LensEmulator/ +| |-- Config.h Set DEBUG, DEBUG_TIMING, LISTEN_ONLY +| +-- LensEmulator.ino ++-- BodyEmulator/ + |-- Config.h Set DEBUG, DEBUG_TIMING, VD_FREQUENCY_HZ + +-- BodyEmulator.ino +``` + +## Protocol documentation +[Google Doc -- E-Mount protocol reverse engineering](https://docs.google.com/document/d/1iw54nzrF0bzQgLINpcP9F8Odd0N5cd7LjlwCDPTNZK0/edit#) ## Sample data See [LexOptical/E-Mount-Traffic-Samples](https://github.com/LexOptical/E-Mount-Traffic-Samples) ## Building -You need to setup the [Teensyduino add-ons](https://www.pjrc.com/teensy/td_download.html) for Arduino Studio. The project should then build in the Arduino Studio normally. +1. Install the [Teensyduino add-ons](https://www.pjrc.com/teensy/td_download.html) for Arduino IDE 2.x. +2. Open either `LensEmulator/LensEmulator.ino` or `BodyEmulator/BodyEmulator.ino` in the IDE. +3. Edit the sketch's `Config.h` to set your desired mode flags. +4. Select **Teensy 3.5** as the board and upload. ## Electrical interface -To interface with a camera you can construct a jig using a macro extension tube (use a 16mm one if possible as the wiring is difficult in a 10mm version). Meike brand is known to be workable. There is no logic level translation needed or external components + +To interface with a camera you can construct a jig using a macro extension tube (use a 16mm one +if possible as the wiring is difficult in a 10mm version). Meike brand is known to be workable. +There is no logic level translation needed or external components. ![Circuit Diagram](/circuitdiagram.png) -Appologies for the quality of the above diagram (it's easier than modeling the whole Teensy in Eagle) +> **Pin assignments by mode** (see also [issue #1](https://github.com/LexOptical/E-Mount/issues/1)): + +| Mode | PIN0 (Serial1 RX) | PIN1 (Serial1 TX) | PIN9 (Serial2 RX) | PIN10 (Serial2 TX) | PIN4 | +|------|-------------------|-------------------|-------------------|--------------------|------| +| LISTEN_ONLY | LENS7 (lens->body) | unused (PIN5) | BODY7 (body->lens) | -- | INPUT (VD in) | +| LensEmulator | body->lens (read) | LENS7 (lens->body write) | BODY7 (body->lens) | -- | INPUT (VD in) | +| BodyEmulator | LENS7 (lens->body read) | unused (PIN5) | -- | BODY7 (body->lens write) | OUTPUT (VD out) | + +> The circuit diagram above shows LISTEN_ONLY / LensEmulator wiring. +> For BodyEmulator: also connect PIN10 to the BODY7 data line, and connect PIN4 as the VD clock output to the lens. diff --git a/lib/Constants.h b/lib/Constants.h new file mode 100644 index 0000000..ab3cac7 --- /dev/null +++ b/lib/Constants.h @@ -0,0 +1,72 @@ +#ifndef Constants_h +#define Constants_h + +// --------------------------------------------------------------------------- +// Serial pin assignments +// --------------------------------------------------------------------------- +// Serial1 — lens↔body data line (LENS7) +// RX = PIN0 (reads lens→body in LISTEN_ONLY or body emulation) +// TX = PIN1 (writes lens→body in lens emulation) +// TX = PIN5 (routed to unused pin in LISTEN_ONLY mode) +// +// Serial2 — body↔lens data line (BODY7) +// RX = PIN9 (reads body→lens in LISTEN_ONLY or lens emulation) +// TX = PIN10 (writes body→lens in body emulation) +// --------------------------------------------------------------------------- +const int PIN_SERIAL1_RX = 0; +const int PIN_SERIAL1_TX_ACTIVE = 1; +const int PIN_SERIAL1_TX_LISTEN = 5; +const int PIN_SERIAL2_RX = 9; +const int PIN_SERIAL2_TX = 10; + +// --------------------------------------------------------------------------- +// Control signal pins +// --------------------------------------------------------------------------- +const int PIN_LENS_CS_BODY = 2; // CS from lens to body +const int PIN_BODY_CS_LENS = 3; // CS from body to lens +const int PIN_BODY_VD_LENS = 4; // Vertical drive clock (body → lens) + +// --------------------------------------------------------------------------- +// Debug / serial log strings +// --------------------------------------------------------------------------- +const String LENS_TO_BODY = "Lens->Body "; +const String BODY_TO_LENS = "Body->Lens "; +const String INIT_COMPLETE_MSG = "Init Complete"; +const String RESETTING = "RESETTING too many unusedClockWindows"; + +// --------------------------------------------------------------------------- +// Protocol constants +// --------------------------------------------------------------------------- +const int INIT_COMPLETE = 2; +const int INVALID_POSITION = -999; +const int INPUT_BUFFER_SIZE = 256; +const int lensToBody = 1; +const int bodyToLens = 0; + +const int MESSAGE_CLASS_INIT = 0x02; +const int MESSAGE_CLASS_NORMAL = 0x01; + +const int HEADER_LENGTH = 6; // START_BYTE, 16-bit length, class, seq, type +const int FOOTER_LENGTH = 3; // 16-bit checksum, END_BYTE + +const byte START_BYTE = 0xF0; +const byte END_BYTE = 0x55; +const byte VD_DWELL_LOW = 62; // microseconds + +// --------------------------------------------------------------------------- +// Known lens response payloads (lens → body) +// --------------------------------------------------------------------------- +const byte init0B[] = {0xF0, 0x0B, 0x00, 0x02, 0x00, 0x0B, 0x60, 0x00, 0x78, 0x00, 0x55}; +const byte init09[] = {0xF0, 0x14, 0x00, 0x02, 0x00, 0x09, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x55}; +const byte init0D[] = {0xF0, 0x0A, 0x00, 0x02, 0x00, 0x0D, 0x00, 0x19, 0x00, 0x55}; +const byte init10[] = {0xF0, 0x0A, 0x00, 0x02, 0x00, 0x10, 0x00, 0x1C, 0x00, 0x55}; +const byte init0A[] = {0xF0, 0x19, 0x00, 0x02, 0x00, 0x0A, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE2, 0x01, 0x55}; +const byte init01[] = {0xF0, 0x29, 0x00, 0x02, 0x00, 0x01, 0xFF, 0x97, 0x78, 0x15, 0x80, 0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x03, 0x55}; +const byte init08[] = {0xF0, 0xD2, 0x00, 0x02, 0x00, 0x08, 0x55, 0x14, 0x55, 0x14, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xE1, 0x00, 0x03, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xAB, 0x04, 0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x43, 0x00, 0xE0, 0xFF, 0x84, 0xA4, 0x00, 0x00, 0x08, 0x32, 0x25, 0x40, 0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x25, 0x04, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0xF3, 0x07, 0xFB, 0xFB, 0x00, 0x00, 0x00, 0x04, 0x04, 0x20, 0x46, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x09, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0x80, 0xDD, 0x52, 0x00, 0xD0, 0xDD, 0xDF, 0xF0, 0x00, 0x08, 0xA0, 0xDD, 0xE0, 0x00, 0x52, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x64, 0x27, 0x00, 0x06, 0x10, 0xFF, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x01, 0x9F, 0x1A, 0x55}; +const byte init07[] = {0xF0, 0x2B, 0x00, 0x02, 0x00, 0x07, 0x01, 0x01, 0x60, 0x01, 0x00, 0x01, 0x01, 0x00, 0xA0, 0x30, 0xC7, 0x00, 0x00, 0x00, 0x00, 0x60, 0x92, 0x86, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x04, 0x55}; + +// Normal-operation lens response templates +const byte norm05[] = {0x55, 0x14, 0x55, 0x14, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0xFF, 0x96, 0x00, 0x96, 0x00, 0x10, 0x01, 0x00, 0x00, 0xB1, 0x28, 0x47, 0x51, 0x4B, 0x38, 0x27, 0x08, 0x35, 0x30, 0x40, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x15, 0x00, 0x00, 0x9A, 0x00, 0x0F, 0x37, 0xD4, 0xC6, 0xC9, 0xDE, 0x0F, 0x37, 0xD4, 0xC6, 0xC9, 0xDE, 0x00}; +const byte norm06[] = {0x10, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x10, 0x3F, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +#endif diff --git a/lib/DebugTools.h b/lib/DebugTools.h new file mode 100644 index 0000000..8df824d --- /dev/null +++ b/lib/DebugTools.h @@ -0,0 +1,51 @@ +#ifndef DebugTools_h +#define DebugTools_h + +byte debugOutputBuffer[INPUT_BUFFER_SIZE] = {0}; +int debugOutputBufferPosition = 0; + +void printHexBuffer(byte* buffer, int length) { + char tmp[3]; + for (int i = 0; i < length; i++) { + sprintf(tmp, "%.2X", buffer[i]); + Serial.print(tmp); + } +} + +// Write a single byte to the given port, buffering it for debug output. +void writeSerialDebugable(HardwareSerial &port, byte b) { + port.write(b); + if (DEBUG && debugOutputBufferPosition < INPUT_BUFFER_SIZE) { + debugOutputBuffer[debugOutputBufferPosition++] = b; + } +} + +// Write a buffer to the given port, buffering it for debug output. +void writeSerialDebugable(HardwareSerial &port, byte* buffer, int size) { + port.write(buffer, size); + if (DEBUG) { + int space = INPUT_BUFFER_SIZE - debugOutputBufferPosition; + memcpy(&debugOutputBuffer[debugOutputBufferPosition], buffer, min(size, space)); + debugOutputBufferPosition += min(size, space); + } +} + +void writeSerialDebugable(HardwareSerial &port, const byte* buffer, int size) { + writeSerialDebugable(port, (byte*)buffer, size); +} + +// Print and clear the accumulated debug output buffer. +void flushDebugOutputBuffer(const String& label) { + if (DEBUG) { + Serial.print(label); + if (debugOutputBufferPosition > 0) { + printHexBuffer(debugOutputBuffer, debugOutputBufferPosition); + } else { + Serial.print("no message"); + } + debugOutputBufferPosition = 0; + Serial.println(); + } +} + +#endif diff --git a/lib/Message.cpp b/lib/Message.cpp new file mode 100644 index 0000000..1dea028 --- /dev/null +++ b/lib/Message.cpp @@ -0,0 +1,60 @@ +#include "Arduino.h" +#include "Message.h" + + +void printHexBufferForHumans(byte *buffer, int length) { + char tmp[2]; + for (int i = 0; i < length; i++) { + sprintf(tmp, "%.2X", buffer[i]); + Serial.print(tmp); + } +} + +Message::Message(byte *bufferFromWire, int wireLength) { + this->wireLength = wireLength; + bodyLength = wireLength - 9; + messageClass = bufferFromWire[3]; + sequenceNumber = bufferFromWire[4]; + messageType = bufferFromWire[5]; + body = bufferFromWire + 6; + checksum = (bufferFromWire[wireLength - 2] << 8) + bufferFromWire[wireLength - 3]; +} + + +Message::Message(byte messageClass, int sequenceNumber, byte messageType, const byte *body, int bodyLength) { + wireLength = bodyLength + 9; + this->bodyLength = bodyLength; + this->messageClass = messageClass; + this->sequenceNumber = sequenceNumber; + this->messageType = messageType; + this->body = new byte[bodyLength]; + memcpy ( this->body, body, bodyLength); + checksum = 0; +} + +void Message::print() { + printHexBufferForHumans(this->outputBuffer, this->wireLength); +} + +void Message::prepForSending() { + int position = 0; + outputBuffer[position++] = START_BYTE; + outputBuffer[position++] = wireLength & 0xFF; + outputBuffer[position++] = wireLength >> 8; + outputBuffer[position++] = messageClass; + outputBuffer[position++] = (messageClass == MESSAGE_CLASS_NORMAL ? sequenceNumber : 0); + outputBuffer[position++] = messageType; + for (int i = 0; i < bodyLength; i++) { + outputBuffer[position++] = body[i]; + } + + int checksum = 0; + for (int i = 1; i < position; i++) { + checksum += outputBuffer[i]; + } + outputBuffer[position++] = checksum & 0xFF; + outputBuffer[position++] = checksum >> 8; + outputBuffer[position++] = END_BYTE; + + // printHexBufferForHumans(outputBuffer, position); +} diff --git a/lib/Message.h b/lib/Message.h new file mode 100644 index 0000000..cf90918 --- /dev/null +++ b/lib/Message.h @@ -0,0 +1,27 @@ +#ifndef Message_h +#define Message_h + +#include "Arduino.h" +#include "Constants.h" + +class Message { + public: + Message(byte *bufferFromWire, int wireLength); + Message(byte messageClass, int sequenceNumber, byte messageType, const byte *body, int bodyLength); + + void print(); + void prepForSending(); + + int bodyLength; + int wireLength; + byte messageClass; + byte sequenceNumber; + byte messageType; + byte *body; + byte outputBuffer[INPUT_BUFFER_SIZE] = {0}; + + private: + int checksum; +}; + +#endif diff --git a/lib/Message05.cpp b/lib/Message05.cpp new file mode 100644 index 0000000..2203716 --- /dev/null +++ b/lib/Message05.cpp @@ -0,0 +1,24 @@ +#include "Arduino.h" +#include "Message05.h" + +Message05::Message05(byte messageClass, int sequenceNumber, byte messageType, const byte *body, int bodyLength): Message(messageClass, sequenceNumber, messageType, body, bodyLength) { +} + +Message05::Message05(byte *bufferFromWire, int wireLength): Message(bufferFromWire, wireLength) { +} + +void Message05::setAperture(int value) { + aperture = value; + aperture %= 0xAB; // Enforce an arbirary max value for testing + + //Set the bytes in our buffer + body[30] = aperture & 0xFF; + body[31] = aperture >> 8; +} + +void Message05::updateBasedOn03(Message *input) { + if (input->bodyLength > 22) { + body[77] = input->body[21]; + body[78] = input->body[22]; + } +} diff --git a/lib/Message05.h b/lib/Message05.h new file mode 100644 index 0000000..985d311 --- /dev/null +++ b/lib/Message05.h @@ -0,0 +1,18 @@ +#ifndef Message05_h +#define Message05_h + +#include "Arduino.h" +#include "Message.h" + +class Message05 : public Message { + public: + Message05(byte *bufferFromWire, int wireLength); + Message05(byte messageClass, int sequenceNumber, byte messageType, const byte *body, int bodyLength); + + void setAperture(int value); + void updateBasedOn03(Message *input); + + int aperture; +}; + +#endif