err4o4 / tilta-nucleus

Tilta Nucleus N reverse engineering
20 stars 6 forks source link

Tilta Nucleus N TTL UART protocol specs #2

Open mcorrigan opened 1 year ago

mcorrigan commented 1 year ago

Hey I happen to have more specs for the TTL UART protocol for this device, but I'm currently trying to figure out how to tap into the unit (USB, internal pins? my strengths are software). Are you interested in collaborating on this or at least helping me get going?

mcorrigan commented 1 year ago

SERIAL DATA FORMAT 1 - BIT RATE: 115200 2 - DATA LENGTH: 8 BIT 3 - STOP BIT: 1 BIT 4 - PARITY: NONE 5 - COMMUNICATION STANDARD: TTL UART serial Port-3.3V

COMMAND BLOCK DATA CONSTRUCTION :(1 byte): 0x3A MODEL ID (1 byte): 0x01 COMMAND (1 byte): 0x06 PARAM ID (2 bytes): 0x0001 DATA (2 bytes): 0x0001 Checksum (2 bytes): 0xF7 \<CR> \<LF> (2 bytes): 0x0D 0x0A

Refer to Modbus ASCII COMMAND is always 0x06 CHECKSUM Example: Checksum = 0x100- (MODEL ID + COMMAND + ParamID_H+ ParamID_L+ DATA _H+ DATA _L) Checksum = 0x100 - (0x01 + 0x06 + 0x00 + 0x01+ 0x00+ 0x01) =0x100 - 0x09 =0xF7

:010600010001F7

caseybasichis commented 1 year ago

Is the intention to control the Tilta with a wired connection?

mcorrigan commented 1 year ago

Yep

caseybasichis commented 10 months ago

Just picked up a Nano ii, going to attempt wired control this week.

Have you had a chance to test their latest?

philippjeschek commented 10 months ago

You can contact Tilta directly to get the protocol specs for Tilta Nucleus Nano/M motors, but you have to sign a NDA to get it

caseybasichis commented 9 months ago

Tempting, but uneasy signing an NDA with them.

I've been tinkering with GPT4 and the info from this repo, but it looks like the system doesn't register any change with ls /dev/tty* when plugging it in.

Is that expected?

This is the code soup GPT kicked out:

import time
import serial

# Calculate the 8-bit checksum using 2's complement
def calc_checksum(data):
    sum = 0

    # Sum all the bytes in the data
    for byte in data:
        sum += byte

    # Take the 2's complement of the sum
    return (~sum + 1) & 0xFF

def send_command(device_id, cmd, param, data):
    # Build the command
    command = bytearray([device_id, cmd, param >> 8, param & 0xFF, data >> 8, data & 0xFF])
    checksum = calc_checksum(command)
    msg = b":" + bytes(command) + bytes([checksum]) + b"\r\n"

    # Send the command
    ser.write(msg)

ser = serial.Serial(
    port='/dev/ttyS9',\
    baudrate=115200,\
    parity=serial.PARITY_NONE,\
    stopbits=serial.STOPBITS_ONE,\
    bytesize=serial.EIGHTBITS,\
    timeout=0)

print("Connected to: " + ser.portstr)

device_id = 1
cmd_focus = 5
param_speed = 0
data_min = 0
data_max = 9999

while True:
    try:
        # Send focus command with increasing data value
        for data in range(data_min, data_max):
            send_command(device_id, cmd_focus, param_speed, data)
            time.sleep(0.01)  # Adjust this sleep time as needed for the desired movement speed

        # Send focus command with decreasing data value
        for data in range(data_max, data_min, -1):
            send_command(device_id, cmd_focus, param_speed, data)
            time.sleep(0.01)  # Adjust this sleep time as needed for the desired movement speed

    except KeyboardInterrupt:
        print("Closing...")
        ser.close()
        break

Are any of you using this day to day?

maxholgasson commented 7 months ago

Tempting, but uneasy signing an NDA with them.

I've been tinkering with GPT4 and the info from this repo, but it looks like the system doesn't register any change with ls /dev/tty* when plugging it in.

Is that expected?

This is the code soup GPT kicked out:

import time
import serial

# Calculate the 8-bit checksum using 2's complement
def calc_checksum(data):
    sum = 0

    # Sum all the bytes in the data
    for byte in data:
        sum += byte

    # Take the 2's complement of the sum
    return (~sum + 1) & 0xFF

def send_command(device_id, cmd, param, data):
    # Build the command
    command = bytearray([device_id, cmd, param >> 8, param & 0xFF, data >> 8, data & 0xFF])
    checksum = calc_checksum(command)
    msg = b":" + bytes(command) + bytes([checksum]) + b"\r\n"

    # Send the command
    ser.write(msg)

ser = serial.Serial(
    port='/dev/ttyS9',\
    baudrate=115200,\
    parity=serial.PARITY_NONE,\
    stopbits=serial.STOPBITS_ONE,\
    bytesize=serial.EIGHTBITS,\
    timeout=0)

print("Connected to: " + ser.portstr)

device_id = 1
cmd_focus = 5
param_speed = 0
data_min = 0
data_max = 9999

while True:
    try:
        # Send focus command with increasing data value
        for data in range(data_min, data_max):
            send_command(device_id, cmd_focus, param_speed, data)
            time.sleep(0.01)  # Adjust this sleep time as needed for the desired movement speed

        # Send focus command with decreasing data value
        for data in range(data_max, data_min, -1):
            send_command(device_id, cmd_focus, param_speed, data)
            time.sleep(0.01)  # Adjust this sleep time as needed for the desired movement speed

    except KeyboardInterrupt:
        print("Closing...")
        ser.close()
        break

Are any of you using this day to day?

Were you sucessful with this attempt? I'm able to open the connection, your python script is properly compiled but the motor doesn't do anything

caseybasichis commented 7 months ago

Not yet.

My next step was to look at the code here -- three of the first generation motors running.. https://github.com/RainbowLabsDE/Fujinon2TiltaNucleus

Does that look usable?

maxholgasson commented 7 months ago

I'll have a look but got it working after some hours

caseybasichis commented 7 months ago

You got the Nano 2 working? I’d love to know the process. My test unit is gathering dust in the hopes that someone gets it going.

maxholgasson commented 7 months ago

It's just the older version nano without 2.

mcorrigan commented 7 months ago

It's just the older version nano without 2.

Did you use the script you posted above? Would be great to have that available.

maxholgasson commented 7 months ago

No the script above posted didnt work for me

mcorrigan commented 7 months ago

No the script above posted didnt work for me

What did you end up doing to get it working?

mcorrigan commented 7 months ago

@maxholgasson this community thing works best when we are learners and sharers :)

ThomasDmr commented 4 months ago

Hi, I know the thread is a bit old, but did anyone manage to get a working code ? @mcorrigan did you manage to connect to the Nucleus ? Otherwise, I might help you with that.

Thanks to all for the shared information !

mcorrigan commented 4 months ago

@ThomasDmr To my knowledge, no one has figured it out yet. tbh, this hasn't been able to be a high enough priority for me with everything else, so I haven't gone back to mess with any of it. If you do get things working though, I know many of us would love to get this figured out.

ThomasDmr commented 4 months ago

Ok, I am currently working on this and this thread has already been of great help, thanks to all of you ! I am quite confident I will make it work, so I will share my code and electronics as soon as I am done. Just hoping I won't have too many bad surprises :)

ThomasDmr commented 4 months ago

Hi guys, I was able to put my hands on a set of Tilta Nucleus M this weekend and managed to make my custom controller work ! Here are the main guidelines to how I did it :

Electrical specs :

Code : I worked with an ESP32 combined with an Arduino framework. My library is C++ and should work on with most C++ compilers, but you will have to remove the Serial.println() debug features that only work in the Arduino environment.

The protocol has been well described by @mcorrigan, but the data is sent as ASCII values and not bytes... so the hex value 0xab will be sent in two ascii bytes corresponding to the characters "a" and "b". This was a surprise but it is coherent with the ASCII Modbus Protocol.

Here is my library :

TiltaSerialProtocol.h

#pragma once

#include "Arduino.h"

// Define protocol specefic variables
#define RAW_MSG_LENGTH      17      // init + 2 model + 2 command + 4 param + 4 data + 2 checksum + CR + LF
#define TOTAL_MSG_BYTES     7       // 1 model, 1 command, 2 param, 2 data, 1 checksum
#define TITLA_INIT_CHAR     0x3A
#define TILTA_CR_CHAR       0x0D
#define TILTA_LF_CHAR       0x0A

// Define constant values for Iris, Focus and Zoom messages
#define TILTA_FOCUS_MODEL   1
#define TILTA_IRIS_MODEL    2
#define TILTA_ZOOM_MODEL    4
#define TILTA_COMMAND       6
#define TILTA_PARAM         1280

// Define the message structure of the the numerical output data
typedef union
{
    struct __attribute__((__packed__))
    {
        uint8_t     model;
        uint8_t     command;
        uint16_t    param;
        uint16_t    data;
        uint8_t     checksum;
    };
    uint8_t raw[TOTAL_MSG_BYTES];
} numerical_packet;

// Define the message structure of input ASCII data
typedef union
{
    struct __attribute__((__packed__))
    {
        char     model[2];
        char     command[2];
        char     param[4];
        char     data[4];
        char     checksum[2];
    };
    char raw[RAW_MSG_LENGTH - 3];
} ascii_packet;

// Class managing the Tilta UART protocol
class TiltaSerialProtocol
{
    public:

    TiltaSerialProtocol();
    bool        parseNewSerialData(char newSerialData[], int dataLength);
    uint8_t*    encodeNewMessage(uint8_t model, uint8_t command, uint16_t param, uint16_t data);
    uint8_t*    encodeNewFocus(uint16_t data);
    uint8_t*    encodeNewIris(uint16_t data);
    uint8_t*    encodeNewZoom(uint16_t data);

    // Setter functions
    void        setDebug(bool debug);
    void        setModel(uint8_t model);
    void        setCommand(uint8_t command);
    void        setParam(uint16_t param);
    void        setData(uint16_t data);

    // Getter functions
    uint8_t     getModel() const;
    uint8_t     getCommand() const;
    uint16_t    getParam() const;
    uint16_t    getData() const;

    private:

    numerical_packet    m_numPkt;
    ascii_packet        m_asciiPkt;
    bool                m_debug;

    uint8_t     m_computeChecksum(uint8_t *data, int len); // not used
    uint8_t     m_computeChecksum();
    uint16_t    m_swapEndian(uint16_t valueToSwap) const;    // not used
    unsigned    m_charToHex(char character);
    unsigned    m_charArrayToHex(const char* charArray, size_t size);
    void        m_convertAsciiToHex();
    char        m_hexToChar(unsigned hexValue);
    void        m_hexToCharArray(unsigned hexValue, char* charArray, size_t size);
    void        m_convertHexToAscii();
};

TiltaSerialProtocol.cpp

#include "TiltaSerialProtocol.h"

TiltaSerialProtocol::TiltaSerialProtocol() : m_debug(false)
{
    // Initialize all values to "-1"
    m_numPkt.model = -1;
    m_numPkt.command = -1;
    m_numPkt.param = -1;
    m_numPkt.data = -1;
}

uint8_t     TiltaSerialProtocol::getModel() const
{
    return m_numPkt.model;
}

uint8_t     TiltaSerialProtocol::getCommand() const
{
    return m_numPkt.command;
}

uint16_t    TiltaSerialProtocol::getParam() const
{
    return (m_numPkt.param);
}

uint16_t    TiltaSerialProtocol::getData() const
{
    return (m_numPkt.data);
}

bool TiltaSerialProtocol::parseNewSerialData(char newMessage[], int messageLength)
{
    if(messageLength != RAW_MSG_LENGTH)
    {
        if(m_debug)
        {
            Serial.println("Serial Message length error : " + String(messageLength));
        }
        return false;
    }
    else
    {
        if(newMessage[0] != TITLA_INIT_CHAR)
        {
            if(m_debug)
            {
                Serial.println("Wrong init char : " + String(newMessage[0]));
            }
            return false;
        }
        else if(newMessage[RAW_MSG_LENGTH - 2] != TILTA_CR_CHAR || newMessage[RAW_MSG_LENGTH - 1] != TILTA_LF_CHAR)
        {
            if(m_debug)
            {
                Serial.println("Wrong end chars : " + String(newMessage[RAW_MSG_LENGTH - 2], HEX) + " | " + String(newMessage[RAW_MSG_LENGTH - 1], HEX));
            }
            return false;
        }
        else
        {
            for(int i = 0; i < RAW_MSG_LENGTH - 3; i++)
            {
                m_asciiPkt.raw[i] = newMessage[i+1];
            }

            // Convert the ascii data to its equivalent numerical data
            m_convertAsciiToHex();

            // for (int j = 0; j < 7; j++)
            // {
            //     Serial.print(String(m_numPkt.raw[j], HEX) + "|");
            // }
            // Serial.println();

            uint8_t checksum = m_computeChecksum();

            if(checksum != (uint8_t)m_numPkt.checksum)
            {
                if(m_debug)
                {
                    Serial.println("Wrong checksum : " + String(m_numPkt.checksum, HEX) + " vs " + String(checksum, HEX));
                }
                return false;
            }

            return true;
        }
    }
}

uint8_t* TiltaSerialProtocol::encodeNewMessage(uint8_t model, uint8_t command, uint16_t param, uint16_t data)
{
    // Store the data to be sent in the numerical packet
    m_numPkt.model     = model;
    m_numPkt.command   = command;
    m_numPkt.param     = param;
    m_numPkt.data      = data;
    m_numPkt.checksum  = this->m_computeChecksum();

    static uint8_t dataToSend[RAW_MSG_LENGTH];
    dataToSend[0] = TITLA_INIT_CHAR; // First char is ":"
    dataToSend[RAW_MSG_LENGTH - 2] = TILTA_CR_CHAR; // <CR>
    dataToSend[RAW_MSG_LENGTH - 1] = TILTA_LF_CHAR; // <LF>

    // Convert the numeriral packet to the ascii packet
    m_convertHexToAscii();

    for(int i = 0; i < RAW_MSG_LENGTH - 3; i++)
    {
        dataToSend[i + 1] = m_asciiPkt.raw[i];
    }

    // Serial.print("New Message : ");
    // for (int j = 0; j < 17; j++)
    // {
    //     Serial.write(dataToSend[j]);
    // }
    // Serial.println();

    return dataToSend;
}

uint8_t* TiltaSerialProtocol::encodeNewFocus(uint16_t data)
{
    return this->encodeNewMessage(TILTA_FOCUS_MODEL, TILTA_COMMAND, TILTA_PARAM, data);
}

uint8_t* TiltaSerialProtocol::encodeNewIris(uint16_t data)
{
    return this->encodeNewMessage(TILTA_IRIS_MODEL, TILTA_COMMAND, TILTA_PARAM, data);
}

uint8_t* TiltaSerialProtocol::encodeNewZoom(uint16_t data)
{
    return this->encodeNewMessage(TILTA_ZOOM_MODEL, TILTA_COMMAND, TILTA_PARAM, data);
}

void TiltaSerialProtocol::setDebug(bool debug)
{
    m_debug = debug;
}

void TiltaSerialProtocol::setModel(uint8_t model)
{
    m_numPkt.model = model;
}

void TiltaSerialProtocol::setCommand(uint8_t command)
{
    m_numPkt.command = command;
}

void TiltaSerialProtocol::setParam(uint16_t param)
{
    m_numPkt.param = param;
}

void TiltaSerialProtocol::setData(uint16_t data)
{
    m_numPkt.data = data;
}

// Calculate the 8-bit checksum using 2's complement
uint8_t TiltaSerialProtocol::m_computeChecksum(uint8_t *data, int len)
{
    int sum = 0x100;

    for (int i = 0; i < len; i++)
    {
        sum -= data[i];
    }
    return (uint8_t)(sum);
}

// Calculate the 8-bit checksum using 2's complement
uint8_t TiltaSerialProtocol::m_computeChecksum()
{
    int sum = 0x100;

    for (int i = 0; i < TOTAL_MSG_BYTES - 1; i++)
    {
        sum -= m_numPkt.raw[i];
    }
    return (uint8_t)(sum);
}

uint16_t TiltaSerialProtocol::m_swapEndian(uint16_t valueToSwap) const
{
    return (valueToSwap << 8) | (valueToSwap >> 8);
}

unsigned TiltaSerialProtocol::m_charToHex(char character) {
    if (character >= '0' && character <= '9')
        return character - '0';
    else if (character >= 'A' && character <= 'F')
        return character - 'A' + 10;
    else if (character >= 'a' && character <= 'f')
        return character - 'a' + 10;
    else
        return 0; // Invalid character
}

unsigned TiltaSerialProtocol::m_charArrayToHex(const char* charArray, size_t size) {
    unsigned hexValue = 0;
    for (size_t i = 0; i < size; ++i) {
        hexValue <<= 4; // Shift left by 4 bits
        hexValue += m_charToHex(charArray[i]);
    }
    return hexValue;
}

void TiltaSerialProtocol::m_convertAsciiToHex()
{
    m_numPkt.model = m_charArrayToHex(m_asciiPkt.model, 2);
    m_numPkt.command = m_charArrayToHex(m_asciiPkt.command, 2);
    m_numPkt.param = m_charArrayToHex(m_asciiPkt.param, 4);
    m_numPkt.data = m_charArrayToHex(m_asciiPkt.data, 4);
    m_numPkt.checksum = m_charArrayToHex(m_asciiPkt.checksum, 2);
}

char TiltaSerialProtocol::m_hexToChar(unsigned hexValue) {
    if (hexValue >= 0 && hexValue <= 9)
        return '0' + hexValue;
    else if (hexValue >= 10 && hexValue <= 15)
        return 'A' + (hexValue - 10);
    else
        return '\0'; // Invalid hexadecimal value
}

void TiltaSerialProtocol::m_hexToCharArray(unsigned hexValue, char* charArray, size_t size) {
    for (size_t i = size; i > 0; --i) {
        charArray[i - 1] = m_hexToChar(hexValue & 0xF); // Extract the last 4 bits
        hexValue >>= 4; // Shift right by 4 bits
    }
}

void TiltaSerialProtocol::m_convertHexToAscii()
{
    char charArray[4];
    m_hexToCharArray(m_numPkt.model, charArray, 2);
    m_asciiPkt.model[0] = charArray[0];
    m_asciiPkt.model[1] = charArray[1];

    m_hexToCharArray(m_numPkt.command, charArray, 2);
    m_asciiPkt.command[0] = charArray[0];
    m_asciiPkt.command[1] = charArray[1];

    m_hexToCharArray(m_numPkt.param, charArray, 4);
    m_asciiPkt.param[0] = charArray[0];
    m_asciiPkt.param[1] = charArray[1];
    m_asciiPkt.param[2] = charArray[2];
    m_asciiPkt.param[3] = charArray[3];

    m_hexToCharArray(m_numPkt.data, charArray, 4);
    m_asciiPkt.data[0] = charArray[0];
    m_asciiPkt.data[1] = charArray[1];
    m_asciiPkt.data[2] = charArray[2];
    m_asciiPkt.data[3] = charArray[3];

    m_hexToCharArray(m_numPkt.checksum, charArray, 2);
    m_asciiPkt.checksum[0] = charArray[0];
    m_asciiPkt.checksum[1] = charArray[1];
}
mcorrigan commented 4 months ago

Hey that's awesome @ThomasDmr, thanks for sharing! The Nucleus N doesn't have the same LEMO connector as the M, unfortunately, I have the Nucleus N. Did you come across anything that might indicate how to tap into the N by chance? Again, I really appreciate you sharing, it looks incredible.

ThomasDmr commented 4 months ago

@mcorrigan I hope the code will help you and others, as the thread already has helped me so far 👍 I have only worked on the Nucleus M for a specific project and don't know much about the Tilta products. Is the Tilta N the same as the Tilta Nano ? I have checked the manual, and it looks like it is only wireless communication, am I correct ?

mcorrigan commented 4 months ago

Yeah I think those are the same thing, and yeah it's wireless. With the name of the repo, I was hoping to find a way to tap into that, even at a hardware level (cracking it open and wiring directly into the PCB).

caseybasichis commented 3 months ago

I ordered a Nucleus M — testing when I get back in town.

I also have a nano II and the mini mirage matte box motor. Does this look promising?

https://www.amazon.com/gp/product/B0CTHMT6PR/ref=ox_sc_act_title_2?smid=A3J2C6Y3UFHRF4&psc=1&fbclid=IwZXh0bgNhZW0CMTEAAR0LFlzxkQjK_fuOIw3SHUDW8_mz1Aya2YBGZ2LAYbLAYrmXanE-9ZYt3QQ_aem_AReleqasUbPlHfBd0CtRuKWy_THdWLC9wfZUmgHq715VQx_zquD6EOq9KkeQzSrwB0F7JJstKN7MffSlIaKZaVis

https://www.facebook.com/groups/REDKomodoUsers/permalink/1241421903407551/?mibextid=W9rl1R

mcorrigan commented 3 months ago

I ordered a Nucleus M — testing when I get back in town.

I also have a nano II and the mini mirage matte box motor. Does this look promising?

https://www.amazon.com/gp/product/B0CTHMT6PR/ref=ox_sc_act_title_2?smid=A3J2C6Y3UFHRF4&psc=1&fbclid=IwZXh0bgNhZW0CMTEAAR0LFlzxkQjK_fuOIw3SHUDW8_mz1Aya2YBGZ2LAYbLAYrmXanE-9ZYt3QQ_aem_AReleqasUbPlHfBd0CtRuKWy_THdWLC9wfZUmgHq715VQx_zquD6EOq9KkeQzSrwB0F7JJstKN7MffSlIaKZaVis

https://www.facebook.com/groups/REDKomodoUsers/permalink/1241421903407551/?mibextid=W9rl1R

Hmm, I'm not sure. The Nucleus N is at work, but to memory it was a micro usb connection. But still, if I could get my hands on one of these cables, we could see if it is a passthrough patch cable or if there is additional circuitry in it. like a RS232->TTL or something.

ThomasDmr commented 3 months ago

Yeah I think those are the same thing, and yeah it's wireless. With the name of the repo, I was hoping to find a way to tap into that, even at a hardware level (cracking it open and wiring directly into the PCB).

Sorry for the late reply, I was on vacation.

I don't know anything about the wireless protocol for the nucleus, I only discovered their existence a few weeks ago for a project I'm doing for someone else. If you want to control the Nucleus N you will sadly have to dig deeper into the wireless communication. Is it WiFi, Bluetooth or something else ? Not sure I will be of great help to you with that, I'm sorry.

ThomasDmr commented 3 months ago

I ordered a Nucleus M — testing when I get back in town.

I also have a nano II and the mini mirage matte box motor. Does this look promising?

https://www.amazon.com/gp/product/B0CTHMT6PR/ref=ox_sc_act_title_2?smid=A3J2C6Y3UFHRF4&psc=1&fbclid=IwZXh0bgNhZW0CMTEAAR0LFlzxkQjK_fuOIw3SHUDW8_mz1Aya2YBGZ2LAYbLAYrmXanE-9ZYt3QQ_aem_AReleqasUbPlHfBd0CtRuKWy_THdWLC9wfZUmgHq715VQx_zquD6EOq9KkeQzSrwB0F7JJstKN7MffSlIaKZaVis

https://www.facebook.com/groups/REDKomodoUsers/permalink/1241421903407551/?mibextid=W9rl1R

I'm not an expert in Nucleus hardware, but this cable looks like it is mainly for power supply. I see only 3 connectors on one side which doesn't leave any place for data transmission (one would be GND, another VCC and the third I don't know).

mcorrigan commented 3 months ago

Yeah I also noticed it has 3 pins on the connector, I assumed the last one is a half duplex data pin, but I don't know without throwing a scope on it.

wallydeedo commented 3 months ago

The nucleus n Motor has a small jack socket. I believe maybe standard 3.3V TTL UART .. The Tilta N, M wireless hardware uses Zigbee pro ‘Off-The-Shelf’ serial modules…