Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions lib/furble/Camera.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ Camera::~Camera() {
m_Client = nullptr;
}

time_t Camera::toUnixTime(const timesync_t &timesync) {
struct tm tm = {
.tm_sec = static_cast<int>(timesync.second),
.tm_min = static_cast<int>(timesync.minute),
.tm_hour = static_cast<int>(timesync.hour),
.tm_mday = static_cast<int>(timesync.day),
.tm_mon = static_cast<int>(timesync.month - 1),
.tm_year = static_cast<int>(timesync.year - 1900),
.tm_wday = 0,
.tm_yday = 0,
.tm_isdst = -1,
};
return mktime(&tm);
}

void Camera::onConnect(NimBLEClient *pClient) {
ESP_LOGI(LOG_TAG, "Connected, adjusting transmit power to %d", m_Power);
// Set BLE transmit power after connection is established.
Expand Down
7 changes: 7 additions & 0 deletions lib/furble/Camera.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef CAMERA_H
#define CAMERA_H

#include <time.h>
#include <atomic>
#include <mutex>

Expand Down Expand Up @@ -32,6 +33,7 @@ class Camera: public NimBLEClientCallbacks {
SONY = 7,
FUJIFILM_SECURE = 8,
RICOH = 9,
PANASONIC_LUMIX = 10,
};

enum class PairType : uint8_t {
Expand Down Expand Up @@ -67,6 +69,11 @@ class Camera: public NimBLEClientCallbacks {
unsigned int centisecond;
} timesync_t;

/**
* Convert a time sync structure to Unix epoch seconds.
*/
static time_t toUnixTime(const timesync_t &timesync);

Camera();
~Camera();

Expand Down
8 changes: 8 additions & 0 deletions lib/furble/CameraList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "FauxNY.h"
#include "FujifilmBasic.h"
#include "FujifilmSecure.h"
#include "Lumix.h"
#include "Nikon.h"
#include "Ricoh.h"
#include "Sony.h"
Expand Down Expand Up @@ -185,6 +186,10 @@ void CameraList::load(void) {
m_ConnectList.push_back(
std::make_unique<Furble::FujifilmSecure>(static_cast<const void *>(dbuffer), dbytes));
break;
case Camera::Type::PANASONIC_LUMIX:
m_ConnectList.push_back(
std::make_unique<Furble::Lumix>(static_cast<const void *>(dbuffer), dbytes));
break;
}
}
m_Prefs.end();
Expand Down Expand Up @@ -243,6 +248,9 @@ bool CameraList::match(const NimBLEAdvertisedDevice *pDevice) {
} else if (FujifilmSecure::matches(pDevice)) {
m_ConnectList.push_back(std::make_unique<Furble::FujifilmSecure>(pDevice));
return true;
} else if (Lumix::matches(pDevice)) {
m_ConnectList.push_back(std::make_unique<Furble::Lumix>(pDevice));
return true;
}

return false;
Expand Down
15 changes: 1 addition & 14 deletions lib/furble/CanonEOSSmart.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,6 @@ void CanonEOSSmart::focusRelease(void) {

void CanonEOSSmart::updateGeoData(const gps_t &gps, const timesync_t &timesync) {
if (m_GeoEnabled) {
struct tm time_str = {
.tm_sec = static_cast<int>(timesync.second),
.tm_min = static_cast<int>(timesync.minute),
.tm_hour = static_cast<int>(timesync.hour),
.tm_mday = static_cast<int>(timesync.day),
.tm_mon = static_cast<int>(timesync.month - 1),
.tm_year = static_cast<int>(timesync.year - 1900),
.tm_wday = 0,
.tm_yday = 0,
.tm_isdst = -1,
};

time_t timestamp = mktime(&time_str);
canon_geo_t geo = {
.header = 0x04,
.latitude_direction = gps.latitude < 0.0 ? 'S' : 'N',
Expand All @@ -214,7 +201,7 @@ void CanonEOSSmart::updateGeoData(const gps_t &gps, const timesync_t &timesync)
.longitude = static_cast<float>(std::abs(gps.longitude)),
.elevation_sign = gps.altitude < 0.0 ? '-' : '+',
.elevation = static_cast<float>(std::abs(gps.altitude)),
.timestamp = static_cast<uint32_t>(timestamp),
.timestamp = static_cast<uint32_t>(toUnixTime(timesync)),
};
if ((m_Geo != nullptr) && m_Geo->canWrite()) {
m_Geo->writeValue(reinterpret_cast<const uint8_t *>(&geo), sizeof(geo), true);
Expand Down
218 changes: 218 additions & 0 deletions lib/furble/Lumix.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#include <NimBLEAddress.h>
#include <NimBLEAdvertisedDevice.h>
#include <NimBLEDevice.h>
#include <NimBLERemoteCharacteristic.h>
#include <NimBLERemoteService.h>

#include "Device.h"
#include "Lumix.h"

namespace Furble {

const NimBLEUUID Lumix::SESSION_SVC_UUID {0x054ac620, 0x3214, 0x11e6, 0xac0d0002a5d5c51b};
const NimBLEUUID Lumix::SESSION_INIT_UUID {0x8d08a420, 0x3213, 0x11e6, 0x8aca0002a5d5c51b};
const NimBLEUUID Lumix::DEVICE_NAME_UUID {0xcd7a71a0, 0x3213, 0x11e6, 0x8f560002a5d5c51b};
const NimBLEUUID Lumix::CONTROL_SVC_UUID {0x7be5faae, 0x475b, 0x11e7, 0xa91992ebcb67fe33};
const NimBLEUUID Lumix::CONTROL_UUID {0x7be5fd56, 0x475b, 0x11e7, 0xa91992ebcb67fe33};
const NimBLEUUID Lumix::LOCATION_SVC_UUID {0x1d74afe0, 0x3214, 0x11e6, 0x8ab40002a5d5c51b};
const NimBLEUUID Lumix::LOCATION_CHR_UUID {0xdaff1bc0, 0x3216, 0x11e6, 0x91c80002a5d5c51b};
const NimBLEUUID Lumix::CLOCK_SVC_UUID {0x34738720, 0x3214, 0x11e6, 0xb66b0002a5d5c51b};
const NimBLEUUID Lumix::CLOCK_CHR_UUID {0xead55e60, 0x3216, 0x11e6, 0xa42e0002a5d5c51b};

// Seconds from 1970-01-01 (Unix epoch) to 1980-01-06 (GPS epoch).
static constexpr uint32_t GPS_EPOCH_OFFSET = 315964800;

Lumix::Lumix(const void *data, size_t len) : Camera(Type::PANASONIC_LUMIX, PairType::SAVED) {
if (len != sizeof(lumix_t))
abort();

const lumix_t *lumix = static_cast<const lumix_t *>(data);
m_Name = std::string(lumix->name);
m_Address = NimBLEAddress(lumix->address, lumix->type);
}

Lumix::Lumix(const NimBLEAdvertisedDevice *pDevice) : Camera(Type::PANASONIC_LUMIX, PairType::NEW) {
m_Name = pDevice->getName();
if (m_Name.empty())
m_Name = "LUMIX";
m_Address = pDevice->getAddress();
ESP_LOGI(LOG_TAG, "Lumix Name = %s", m_Name.c_str());
ESP_LOGI(LOG_TAG, "Lumix Address = %s", m_Address.toString().c_str());
}

bool Lumix::matches(const NimBLEAdvertisedDevice *pDevice) {
if (pDevice->haveManufacturerData() && pDevice->getManufacturerData().size() >= sizeof(adv_t)) {
const adv_t adv = pDevice->getManufacturerData<adv_t>();
if (adv.company_id != ADV_PANASONIC_ID)
return false;
} else {
return false;
}
return pDevice->isAdvertisingService(SESSION_SVC_UUID);
}

bool Lumix::_connect(void) {
m_Progress = 10;
ESP_LOGI(LOG_TAG, "Lumix connecting");
if (!m_Client->connect(m_Address)) {
ESP_LOGI(LOG_TAG, "Lumix connection failed");
return false;
}
m_Progress = 30;

// MEI0 handshake
NimBLERemoteService *pSvc = m_Client->getService(SESSION_SVC_UUID);
if (pSvc == nullptr) {
ESP_LOGI(LOG_TAG, "Lumix session service unavailable");
return false;
}

NimBLERemoteCharacteristic *pChr = pSvc->getCharacteristic(SESSION_INIT_UUID);
if (pChr == nullptr || !pChr->canWrite()) {
ESP_LOGI(LOG_TAG, "Lumix session-init characteristic unavailable");
return false;
}
if (!pChr->writeValue(MEI0.data(), MEI0.size(), true)) {
ESP_LOGI(LOG_TAG, "Lumix MEI0 write failed");
return false;
}
m_Progress = 50;

// Device name
pChr = pSvc->getCharacteristic(DEVICE_NAME_UUID);
if (pChr == nullptr || !pChr->canWrite()) {
ESP_LOGI(LOG_TAG, "Lumix device-name characteristic unavailable");
return false;
}
// Send identity
const std::string name = Device::getStringID();
if (!pChr->writeValue(name.c_str(), name.length(), true)) {
ESP_LOGI(LOG_TAG, "Lumix device-name write failed");
return false;
}
m_Progress = 70;

// Remote control service.
pSvc = m_Client->getService(CONTROL_SVC_UUID);
if (pSvc == nullptr) {
ESP_LOGI(LOG_TAG, "Lumix control service unavailable");
return false;
}
m_Control = pSvc->getCharacteristic(CONTROL_UUID);
if (m_Control == nullptr || !m_Control->canWrite()) {
ESP_LOGI(LOG_TAG, "Lumix control characteristic unavailable");
return false;
}

// Location and clock services (optional).
pSvc = m_Client->getService(LOCATION_SVC_UUID);
if (pSvc != nullptr) {
m_Location = pSvc->getCharacteristic(LOCATION_CHR_UUID);
} else {
ESP_LOGI(LOG_TAG, "Lumix location service unavailable");
}
pSvc = m_Client->getService(CLOCK_SVC_UUID);
if (pSvc != nullptr) {
m_Clock = pSvc->getCharacteristic(CLOCK_CHR_UUID);
} else {
ESP_LOGI(LOG_TAG, "Lumix clock service unavailable");
}

m_Progress = 100;
ESP_LOGI(LOG_TAG, "Lumix connected");
return true;
}

void Lumix::_disconnect(void) {
m_Control = nullptr;
m_Location = nullptr;
m_Clock = nullptr;
if (m_Client != nullptr && m_Client->isConnected())
m_Client->disconnect();
}

bool Lumix::writeCommand(uint8_t command) {
if (!isConnected() || m_Control == nullptr || !m_Control->canWrite()) {
ESP_LOGW(LOG_TAG, "Lumix control write skipped: command=0x%02x connected=%d control=%p",
command, isConnected(), m_Control);
return false;
}
if (!m_Control->writeValue(&command, sizeof(command), true)) {
ESP_LOGW(LOG_TAG, "Lumix control write failed: command=0x%02x", command);
return false;
}
return true;
}

void Lumix::shutterPress(void) {
writeCommand(CMD_SHUTTER_PRESS);
}

void Lumix::shutterRelease(void) {
writeCommand(CMD_SHUTTER_RELEASE);
}

void Lumix::focusPress(void) {
writeCommand(CMD_FOCUS_PRESS);
}

void Lumix::focusRelease(void) {
writeCommand(CMD_FOCUS_RELEASE);
}

void Lumix::updateGeoData(const gps_t &gps, const timesync_t &timesync) {
if (!isConnected()) {
ESP_LOGW(LOG_TAG, "Lumix GPS skipped: not connected");
return;
}
if (m_Location == nullptr || !m_Location->canWrite()) {
ESP_LOGW(LOG_TAG, "Lumix GPS characteristic unavailable");
return;
}

geo_t geo = {
.gps_time = static_cast<uint32_t>(toUnixTime(timesync)) - GPS_EPOCH_OFFSET,
.latitude = static_cast<int32_t>(gps.latitude * 10000000.0),
.longitude = static_cast<int32_t>(gps.longitude * 10000000.0),
.altitude = static_cast<uint16_t>(gps.altitude),
.fix = 0x41, // 'A' — NMEA valid fix
.reserved = 0x00,
};
bool rc = m_Location->writeValue(reinterpret_cast<const uint8_t *>(&geo), sizeof(geo), true);

// Synchronise the camera clock when a write characteristic is available.
if (rc && m_Clock != nullptr && m_Clock->canWrite()) {
clock_t clock = {
.year = static_cast<uint16_t>(timesync.year),
.month = static_cast<uint8_t>(timesync.month),
.day = static_cast<uint8_t>(timesync.day),
.hour = static_cast<uint8_t>(timesync.hour),
.minute = static_cast<uint8_t>(timesync.minute),
.second = static_cast<uint8_t>(timesync.second),
.zero = 0x00,
.tz_offset = 0,
};
m_Clock->writeValue(reinterpret_cast<const uint8_t *>(&clock), sizeof(clock), true);
}

ESP_LOGI(LOG_TAG, "Lumix GPS lat=%.7f lon=%.7f alt=%.1f utc=%04u-%02u-%02u %02u:%02u:%02u => %s",
gps.latitude, gps.longitude, gps.altitude, timesync.year, timesync.month, timesync.day,
timesync.hour, timesync.minute, timesync.second, rc ? "ok" : "failed");
}

size_t Lumix::getSerialisedBytes(void) const {
return sizeof(lumix_t);
}

bool Lumix::serialise(void *buffer, size_t bytes) const {
if (bytes != sizeof(lumix_t))
return false;
lumix_t *x = static_cast<lumix_t *>(buffer);
strncpy(x->name, m_Name.c_str(), MAX_NAME);
x->name[MAX_NAME - 1] = '\0';
x->address = static_cast<uint64_t>(m_Address);
x->type = m_Address.getType();
return true;
}

} // namespace Furble
Loading