Open mcorrigan opened 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
Is the intention to control the Tilta with a wired connection?
Yep
Just picked up a Nano ii, going to attempt wired control this week.
Have you had a chance to test their latest?
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
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?
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
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?
I'll have a look but got it working after some hours
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.
It's just the older version nano without 2.
It's just the older version nano without 2.
Did you use the script you posted above? Would be great to have that available.
No the script above posted didnt work for me
No the script above posted didnt work for me
What did you end up doing to get it working?
@maxholgasson this community thing works best when we are learners and sharers :)
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 !
@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.
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 :)
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];
}
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.
@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 ?
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).
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.facebook.com/groups/REDKomodoUsers/permalink/1241421903407551/?mibextid=W9rl1R
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.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.
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.
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.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).
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.
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…
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?