• Система автоматизации с открытым исходным кодом на базе esp8266/esp32 микроконтроллеров и приложения IoT Manager. Наша группа в Telegram

UBIA - USB / BLE to I2C/SMBus Adapter.

Т.е. уже убедились, что самый сложный метод - это делать что либо по Arduino подобию?
Нет, пока не убедился, этот язык я хотя бы понимаю (там все элементарно), а ваш расшифровываю по подобию.
В качестве примера давайте расскажу как я буду исправлять 1Кгц в скорости шины "не-ардуино-подобным" образом (когда имею два образца на непонятном мне языке:)
В тексте примера с датчиком 9250 есть фрагмент кода с такой записью в лог:

log('Send command#01: Init I2C/SMBUS CLK 400 kHz, MPU9250 Reset...'); // - init I2C

ага ! Оно! Данные передаваемые в функцию выглядят так:

new Uint8Array([11, 1, 0, 0, 255, 255, 0x90, 1, 0xD0, 0x6B, 0, 0x80, 0]));

В функции i2cDrvInit(clk) этот блок данных выглядит следующим образом.

let blk = new Uint8Array([6 , 1, 0, 0, 255, 255, clk&0xff, (clk>>8)&0xff]);

Пишем строчки рядом и выравниваем их по параметрам. Видно что первая строчка длиннее второй, по всей видимости хвост это и есть "MPU9250 Reset..."
Начало совпадает кроме первого байта (длина пакета). Затем, после двух 0xFF по всей видимости и идут два байта нужных мне CLK 400 Кбит.
Исправленный вариант функции на мой взгляд должен выглядеть так:
JavaScript:
function i2cDrvInit()
{
   let blk = new Uint8Array([6 , 1, 0, 0, 255, 255, 0x90,1]);
   this.characteristicCache.writeValue(blk);
}
Правильно?

Вроде вы хотели акселерометр на web страничке...
Уже заказал такие же модули аксселерометров, хотелось бы переделать ее под имеющиеся у меня, но я за это возьмусь только когда мы закончим курс молодого бойца.


ps: В процессе стала понятна ваша ранняя фраза "блоками или побайтово?" Получается никто не запрещает мне паковать в одну посылку сразу несколько команд и они в теории выполнятся в пакетном режиме. Так?
 

pvvx

Активный участник сообщества
log('Send command#01: Init I2C/SMBUS CLK 400 kHz, MPU9250 Reset...'); // - init I2C
ага ! Оно! Данные передаваемые в функцию выглядят так:
new Uint8Array([11, 1, 0, 0, 255, 255, 0x90, 1, 0xD0, 0x6B, 0, 0x80, 0]));
0x90, 1 - это два байта в виде слова 0x190, что в DEC будет 400.
Все форматы посылок описаны в Cи хидере исходников.
Чуть позже, когда доделаю тут кое что, то слеплю вам чтение и запись для DS часов. Вот только кому нужен лог на подобии COM порта в Web и как его применить?
 
Вот только кому нужен лог на подобии COM порта в Web и как его применить?
Это простой учебный пример позволяющий понять как запрограммировать модули для работы с вашей прошивкой.
В более сложных примерах новичку очень трудно понять какие части кода за что отвечают. Например если бы я взял в качестве примера MPU9250Cube и попытался на его основе сделать страничку показывающую расстояние с лазерного дальномера. мне бы пришлось:

1) Убирать со страницы весь код связанный с рисованием графики на странице (ведь в моем приложении он не нужен). При условии что я с этой библиотекой раньше не общался, а значит мне придется сначала разобраться как она работает.
2) Убирать весь код связанный с MPU9250Cube, при условии что я не знаю как он программируется, да и вообще туманно представляю как программируется i2c шина как таковая. А значит мне придется сначала разобраться как программируется MPU9250 и шина i2c

И только затем приступать к привинчиванию на оставшуюся часть своего функционала. Думаю 80% новичков-ардуинщиков обломаются в самом начале. Ваша ошибка как популяризатора TSLR имхо заключается в том, что вы заваливаете читателей тоннами вкусной рыбы, а им нужны сети, для того чтобы ловить рыбу самостоятельно.

Чем хорош это лог:

1) В нем нет ничего лишнего, ничего не нужно удалять. Бери и дополняй нужными функциями.
2) В нем использован примитивный широкоизвестный модуль, который легко запрограммировать небольшими кусками простого кода. Вместе с тем, читатель получает опыт посылки и получения команд, а также записи и чтения данных по i2c шине. Этот опыт позволит ему в дальнейшем запрограммировать модуль нужный уже ему. Сам я следующим примером начну программировать свои лазерные измерители расстояния и акселлераторы.

Осталось добавить в копилку примеров еще и код моргания лампочкой на ноге контроллера по нажатию. на нем же кнопки на соседнем выводе через ble (показать использование чипа как устройства управления чем-либо или от чего либо) и можно смело давать вашу либу ардуинщикам. Что им в голову придет реализовать на вашей прошивке вам даже в голову придти не сможет. :) Если же вам хочется чтобы народ ломанулся на саму прошивку напишите для нее пакет а ля ESP-IDF VS Code Extension делали же вы что то подобное для ардуины. Все таки Eclipse в наше время не совсем комильфо.

ps: К слову свой первый "Hello world" для ардуины я написал месяцев так 8 тому назад, до этого вообще никогда микроконтроллеры не программировал. .. ну за исключением периодических всплесков типа этого :)
 
Подправил код, теперь застреваю на самой первой записи

Send command #00: WhoIs?
>> (2) ["0x00", "0x00"]
characteristicvaluechanged
<< (6) ["0x04", "0x00", "0x21", "0x10", "0x08", "0x00"]
Инициализируем I2c шину
Init I2C/SMBUS
>> (8) ["0x06", "0x01", "0x00", "0x00", "0xff", "0xff", "0x90", "0x01"]
characteristicvaluechanged
<< (2) ["0x00", "0x81"]
Отправляю аналог команд Wire.write(0x0E);Wire.write(B00000000)
>> (7) ["0x07", "0x0e", "0x00", "0x00", "0xd0", "0x0e", "0x00"]
 

pvvx

Активный участник сообщества
Это простой учебный пример позволяющий понять как запрограммировать модули для работы с вашей прошивкой.
А UBIA не пример для начинающих. Уж слишком много опций надо знать.
К примеру, если новичек берет Arduino, то у него голова будет болеть пока он не выучит библиотеки и тысячи специфик применения для своего контроллера.

Вот вам готовый извращенный к Arduino зависимым пример к вашим часам i2c, там всё регулируется :)
 

Вложения

Я прошел квест с первым Write !!! Ж)
Правильный ответ получил?


Отправляю аналог команд Wire.write(0x0E);Wire.write(B00000000)
>> (7) ["0x05", "0x0e", "0x00", "0x00", "0xd0", "0x0e", "0x00"]
characteristicvaluechanged
<< (2) ["0x00", "0x0e"]
 

pvvx

Активный участник сообщества
Прям как вы привыкли:
JavaScript:
// установки
const I2C_DEV_ADDR = 0x68;
const I2C_DEV_CLK_KHZ = 400;

var I2C_REG_AINIT = new Uint8Array([0x0E]);
var I2C_REG_DINIT = new Uint8Array([0x00,0x88]);

const I2C_REG_AREAD = new Uint8Array([0]);
const I2C_REG_LREAD = 7;
// главная функция
function ParseI2CDataRead(data) {
     let seconds = data.getUint8(4); // get seconds
     let minutes = data.getUint8(5); // get minutes
     let hours   = data.getUint8(6); // get hours
     let day     = data.getUint8(7);
     let date    = data.getUint8(8);
     let month   = data.getUint8(9); //temp month
     let year    = data.getUint8(10);  
     $("ctime").innerHTML = "Seconds: " + hex(seconds,2) + ", Minutes:" + hex(minutes,2) + ", Hours:" + hex(hours,2) + ", Day:" + hex(day,2) + ", Date:" + hex(date,2) + ", Month:" + hex(month,2) + ", Year:" + hex(year,2);
}
Остальное считайте либой :) Жалко "либу" не подать в бинарном виде, как это делается в Arduino.
 

pvvx

Активный участник сообщества
У вызова ParseI2CDataRead(data) обрезать лишние данные? Т.е. передавать только массив считанных байт?
Или сами справитесь?
 
1) В SendInit посылается 35 байт данных из которых две трети нули. Это с какой то особой целью делается?
2) SetConnParms, ConnParUpdate,GetConnParms обязательны?




Или сами справитесь?
сам справлюсь. Пока разбираюсь .
к слову падает на Ыутв command #4: Get current connect parameters...
Uncaught (in promise) DOMException: GATT operation already in progress.
Как я уже выяснил такое бывает при двух командах подряд.
 

pvvx

Активный участник сообщества
Вот тут посылаются не все нули.
Просто для примера передает 2000 чтений регистров с ваших любимых часов в сек.
 

Вложения

pvvx

Активный участник сообщества
Это не пример на скорость, а просто так для прикола, в смысле как вы любите - микросхема для прикола с логом в Arduino. Но тут лог отключен, а лог эксплорера уже тормозит....
 
в смысле как вы любите
:)
Вот как я люблю (полный проект в приложенном файле):

Основная страничка в ардуино-стайл. Она ничего не знает ни про датчики ни про ble. Чисто юзесркий код с довеском в два класса, ble (основная либа) и ds3231 (код датчика).
JavaScript:
...
<script src="ble.js"></script>
<script src="ds3231.js"></script>
<script>
  var ble;
  var sensor;
  document.addEventListener('DOMContentLoaded', () => {
    sensor = new ds3231({
      log, 
      datetimeChanged:(d) =>{
        document.getElementById("ctime").innerText = d.toLocaleString("ru");
      }
    });
    ble = new Ble({log,
      onerror: (e,info)=>{
        document.getElementById("error").innerHTML+="<li>"+info;
      },
      onsetup: (e) =>{
        sensor.Init(e);
      },
      onloop: (e,idx,value) => {
        // log(value, 'din');
        sensor.Loop(e,idx,value); 
      }
    });
  });
...
В класс Ble в теории можно и не заглядывать, в него я свалил всю внутреннюю кухню, а вот класс под датчик создается конечным пользователем библиотеки. Класс ds3231 генерирует событие при изменении даты, подключившись к нему можно вывести данные на экран.

Ble генерирует события onerror при ошибке, ну и сладкая парочка событий setup и loop никуда не делась, куда уж без них в таком проекте. :)
Setup вызывается после того как инициализируется и настроится i2c шина, датчик там должен провести свою, дополнительную инициализацию о которой не знает ble.
Loop это обработчик ble.characteristicvaluechanged после отработки событий связанных с внутренней кухней библиотеки. По задумкам тут должны оказываться только пользовательские команды и данные и больше ничего, свою кухню библиотека разруливает самостоятельно.

Код датчика ds3231 тоже довольно прост и примитивен. Если спрятать всю его внутреннюю кухню, останутся два основных метода которые и вызываются по событиям ble
JavaScript:
...
 Init(e)
    {
        this.stage_read = 1;
        this._ble=e.ble;
        this.Write(e, new Uint8Array([0x0E]), new Uint8Array([0x00, 0x88]));
    }
    
    Loop(e,idx,value)
    {
        this._ble=e.ble;
        let ds = value.getUint8(0);
        if (idx == 0x0C && ds >= 2)
        {
            if (this.stage_read > 0) 
            {
                if(this.stage_read > 1 && ds >= 9)
                {
                    if(this.datetimeChanged)
                    { 
                        let seconds = this._decode(value.getUint8(4));
                        let minutes = this._decode(value.getUint8(5)); 
                        let hours   = this._decodeH(value.getUint8(6)); 
                        let day     = value.getUint8(7);
                        let date    = this._decode(value.getUint8(8));
                        let month   = this._decode(value.getUint8(9)); 
                        let year    = this._decodeY(value.getUint8(10))+2000;
                        this.datetimeChanged(new Date(year, month, date, hours, minutes, seconds, 0));  
                    }
                }
                if( this._new_date)
                {
                    this.Write(e, new Uint8Array([0x0]), this._new_date);
                    this._new_date=null;
                } else {
                    this.Read(e,new Uint8Array([0]), 7)
                }
                this.stage_read = 2;
            } 
        }
    }
 

Вложения

  • 4.8 KB Просмотры: 4
2000 чтений регистров с ваших любимых часов в сек
Интересно. часам понятное дело это не надо, а вот другим датчикам может пригодится.

В процессе написания кода у меня возникло несколько вопросов. Давайте я их вам по порядку позадаю :)

1) Каким образом можно узнать что некий ответ пришел именно на посланную нами конкретную команду? Или они строго по очереди ходят "запрос-ответ"?
Поясню на примере. Для того чтобы послать команду установки даты на часах, я вызываю метод, который сохраняет подготовленный для передачи массив во внутреннюю переменную объекта. Затем, во время цикла loop, переменная проверяется на пустоту. Если переменная не пуста, отправляется запрос на установку даты и переменная обнуляется, если она изначально была пуста выполняется запрос на чтение даты.
На мой взгляд это как то не кудряво и похоже на костыль. Никак нельзя во время вызова записи добавить какую либо метку, по которйо станет понятно что вот этот конкретный ответ от шины относится именно к нашему запросу?

JavaScript:
 setDateTime(dstr)
{
        var d = new Date(dstr);
        var date_arr =  new Uint8Array([
            this._encode(d.getSeconds()),
            ...
            this._encode(d.getFullYear()-2000) // 34
        ]);
        this._new_date=date_arr;
}

Loop(e,idx,value)
{
  ..
     if( this._new_date)
     {
        this.Write(e, new Uint8Array([0x0]), this._new_date);
        this._new_date=null;
     } else {
        this.Read(e,new Uint8Array([0]), 7)
     }
  ..
}
 

pvvx

Активный участник сообщества
Ble генерирует события onerror при ошибке, ну и сладкая парочка событий setup и loop никуда не делась, куда уж без них в таком проекте. :)
Как-бы всё современное, в чем есть CPU, ныне работает по событиям или типа по калбаскам (callback). Нету ныне setup() и тем более loop(), а сплошная много-задчка-проточка.. :)
Зачем детей учите таким алго и построению программы, что им не пригодится уже никогда?
1) Каким образом можно узнать что некий ответ пришел именно на посланную нами конкретную команду? Или они строго по очереди ходят "запрос-ответ"?
У команды и ответа есть ID, чтобы вы не запутались. И в принципе они ходят запрос-ответ, т.е. так как вы запрограммируете.
В данном случае применен не классический пример для BLE. Причина - совместимость команд с USB и UART (длина пакета там только для синхронизации на пакеты для UART).
У BLE (и USB) существуют шкафы и ящики. У каждого свой номерок ID - UUID. У каждого ящика есть атрибуты - типа он сикретный (подавай пароль или защищенное соединение) и что может: чтение и/или запись. Так-же на ящике может быть флаг - notify.
Если вы его включили, то данные из ящика устройство будет передавать само по своим хотелкам (таймер или обновились, или ...).
В классическом виде работают с ящиками и каждый ящик содержит только свои специфические форматированные данные.
К примеру - Для чтения температуры - свой ящик, для влажности - свой, для параметров соединения - свой, для имени устройства - свой --- номера ящиков и формат данных в них печатает институт стандартов BLE.
Приложение должно открыть дцать ящиков (чтобы побыстрее исчерпать ресурсы дров в системе на дескрипторы к этим ящикам (их не так уж и много выделено почему-то) и увеличить жор у эксплорера, ...
Но допустимо использовать специальный ящик для своих нужд и ковыряться в нем как вам нравиться. Вот так и сделано...
 
Нету ныне setup() и тем более loop(), а сплошная много-задчка-проточка..
Дык это и есть эвенты и калбаки. Они просто так называются, рискну предположить что и коде ардуины схема похожая.
У команды и ответа есть ID, чтобы вы не запутались. И
Я имел в виду как мне отличить что эта команда с кодом indx=0xС пришла в ответ на установку времени, а эта с тем же кодом 0xC в ответ на запрос этого времени.
Во время инициализации бегают разные idx коды команд, а потом одни и теже . например если я захочу с этого датчика еще и температуру вывести, как я смогу отличить ответ с температурой от ответа с датой-временем?

или это 0xC это вы сами придумали, и я могу использовать скажем -0xE1 ?
 

pvvx

Активный участник сообщества
Дык это и есть эвенты и калбаки. Они просто так называются, рискну предположить что и коде ардуины схема похожая.

Я имел в виду как мне отличить что эта команда с кодом indx=0xС пришла в ответ на установку времени, а эта с тем же кодом 0xC в ответ на запрос этого времени.
Во время инициализации бегают разные idx коды команд, а потом одни и теже . например если я захочу с этого датчика еще и температуру вывести, как я смогу отличить ответ с температурой от ответа с датой-временем?

или это 0xC это вы сами придумали, и я могу использовать скажем -0xE1 ?
А поток с типа 0x0C всегда запрос- ответ.
Номерки в нутре пакетов с данного ящика я придумал. Можете в исходниках дополнить или изменить на ваше усмотрение. UUID ящик взят из разрешенных по стандарту для таких выкрутасов.
 
речь вот о чем

uint8_t size; //+0 размер данных пакета
uint8_t cmd; //+1 номер команды / тип пакета (=1)

Этот номер команды может быть любым числом или обязательно 0xC как указано тут?


Read(event,reg_addr, len)
{
let blk = new Uint8Array(reg_addr.byteLength + 5);
blk[0] = blk.byteLength - 2;
blk[1] = 0x0C;
blk[2] = reg_addr.byteLength;
blk[3] = (len & 0x7f) | 0x80;



Другими словами, можно сделать 0xC для получения времени и 0xD для получения температуры?
 

pvvx

Активный участник сообщества
Я имел в виду как мне отличить что эта команда с кодом indx=0xС пришла в ответ на установку времени, а эта с тем же кодом 0xC в ответ на запрос этого времени.
Для времени есть стандартные ящики (UUID) со своим форматом от института стандартизации -> https://www.bluetooth.com/specifications/assigned-numbers/
 
это я знаю. вопрос в том можно ли использовать пользовательские uint8_t cmd; //+1 номер команды / тип пакета (=1)
 
Сверху Снизу