Home Assistant devices and uses

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Something so simple!

This is why I build my own stuff. It's trivial to include UTC time stamps with each data sample for time synchronization on controllers without accurate time keeping.
{
"RDAQ1name": "Energy_Mqtt_BMC1",
"RDAQ1board": "BMCBoard",
"RDAQ1sequence": 5893,
"RDAQ1mqtt_do_16b": 1,
"RDAQ1mqtt_di_16b": 0,
"RDAQ1bmc_adc0": 4.9963909422158626,
"RDAQ1bmc_adc1": 3.7562726511828224,
"RDAQ1build_date": "Jun 22 2025",
"RDAQ1build_time": "09:42:01",
"RDAQ1sequence_time": 1750699594
}
{
"RDAQ2name": "Energy_Mqtt_BMC2",
"RDAQ2board": "K8055 (VM110)",
"RDAQ2sequence": 15006,
"RDAQ2mqtt_do_16b": 32,
"RDAQ2mqtt_di_16b": 0,
"RDAQ2bmc_adc0": 31.283194167450276,
"RDAQ2bmc_adc1": 30.052978542849083,
"RDAQ2build_date": "Jun 12 2025",
"RDAQ2build_time": "08:47:14",
"RDAQ2sequence_time": 1750699594
}
{
"RDAQ3name": "Energy_Mqtt_BMC3",
"RDAQ3board": "BMCBoard",
"RDAQ3sequence": 586,
"RDAQ3mqtt_do_16b": 2,
"RDAQ3mqtt_di_16b": 0,
"RDAQ3bmc_adc0": 25.227795128151275,
"RDAQ3bmc_adc1": 31.475574896633365,
"RDAQ3build_date": "Jun 22 2025",
"RDAQ3build_time": "09:42:01",
"RDAQ3sequence_time": 1750699601
}

C:
/*
* check for incoming data on the MQTT connection
* use UART1 or the MQTT receive data port
* calc_bsoc runs in interrupt low context and uses 16-bit results -> git_power from this
* cmd_value is the buffer variable
*/
void gti_cmds(void)
{
    static uint8_t value[] = {0, 0, 0, 0}, vi = 0;
    static uint8_t utc_value[DEF_TIME_SIZE] = {0};
    static bool utc = false;
    uint8_t vcmd_size = sizeof(value);
    uint8_t utc_vcmd_size = DEF_TIME_SIZE - 1;

    if (Sready()) {
        mqtt_r = Sread();
#ifdef GTI_ECHO
        UART1_Write(mqtt_r); // debug echo
#endif

        switch (mqtt_r) {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            if (!utc) { // process power cmds
                if (vi < vcmd_size) {
                    value[vi++] = mqtt_r - 48; // ascii '0'
                } else {
                }
            } else { // process UTC time from host cmds
                if (vi < utc_vcmd_size) {
                    utc_value[vi++] = mqtt_r;
                } else {
                }
            }
            break;
        case 'T': // begin UTC value
            utc = true;
            break;
        case 't': // end UTC value
            utc = false;
            if (vi >= utc_vcmd_size) {
                vi = 0;
                utc_value[10] = 0;
                utc_cmd_value = (time_t) atol((char *) utc_value);

                if (utc_cmd_value < DEF_TIME) {
                    utc_cmd_value = DEF_TIME;
                }
                set_time(utc_cmd_value);
            };
            break;
        case 'V': // begin power/utc value
            vi = 0;
            break;
        case 'X': // end power value
            utc = false;
            if (vi >= vcmd_size) {
                vi = 0;
                cmd_value = value[0]*1000 + value [1]*100 + value[2]*10 + value[3];
                if (cmd_value > GTI_MAX) {
                    cmd_value = GTI_MAX;
                }
                if (cmd_value < 0) {
                    cmd_value = 0;
                }
                INTERRUPT_GlobalInterruptLowDisable(); // 16-bit atomic update
                gti_power = cmd_value;
                INTERRUPT_GlobalInterruptLowEnable();
            };
            break;
        case 'Z': // zero power
            utc = false;
            cmd_value = 0;
            break;
        case '+': // incr power
            utc = false;
            cmd_value = gti_power + GTI_INCR;
            if (cmd_value > GTI_MAX) {
                cmd_value = GTI_MAX;
            }
            break;
        case '-': // decr power
            utc = false;
            cmd_value = gti_power - GTI_INCR;
            if (cmd_value < 0) {
                cmd_value = 0;
            }
            break;
        case 'I': // idle power
            utc = false;
            cmd_value = GTI_IDLE;
            break;
        case 'F': // normal operation
            utc = false;
            cmd_value = GTI_NORM;
            break;
        case 'M': // max unit rated power testing
            utc = false;
            cmd_value = GTI_MAX;
            break;
        case '#': // execute command symbol
            utc = false;
            INTERRUPT_GlobalInterruptLowDisable(); // 16-bit atomic update
            gti_power = cmd_value;
            INTERRUPT_GlobalInterruptLowEnable();
            break;
        default: // eat extra characters
            utc = false;
            while (Sready()) {
                mqtt_r = Sread();
            }
            break;
        }
    }
}

// TMR5 one second time keeping in V.ticks.
void set_time(const time_t t)
{
    PIE8bits.TMR5IE = 0;
    V.ticks = t;
    PIE8bits.TMR5IE = 1;
}

/*
* if t > 0, t is set to memory location of current_time variable
*/
time_t time(time_t * t)
{
    static time_t current_time;
    PIE8bits.TMR5IE = 0;
    current_time = V.ticks;
    PIE8bits.TMR5IE = 1;
    if (t) {
        t = &current_time;
    }
    return current_time;
}
1750701443784.png

The server power system is pushing excess power into the local house grid.
1750702591551.png
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Q84 SPI DAQ data logging.
1751756718177.png

Scalar Index 0 is the SW/HW development system for finishing up the Linux driver to be multi-core for analog and to handle asynchronous Comedilib commands. https://www.comedi.org/doc/asyncprogram.html
1751755909876.png
Q84 remote DAQ, 4 PV voltages from two units (data calibration table Scalar Index 1 and 2).

1751756022555.png
Shed array PV voltage vs GTI input voltage in the house. DC feed-Line voltage drop.

1751756521786.png
Daily running control variables.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Finally finishing off the Q84 DAQ board firmware and the Linux SPI protocol driver for it.
One of the last things to do was to get the remote serial I/O sub-device completed. The simplest way was to use the Comedi MEMORY sub-device framework to memory map the TX, RX and RX ready data in four blocks of 16-bit memory space.

Then use my SPI packet protocol to update the memory blocks.
Linux SPI protocol kernel driver
C:
enum comedi_subdevice_type {
    COMEDI_SUBD_UNUSED,    /* subdevice is unused by driver */
    COMEDI_SUBD_AI,    /* analog input */
    COMEDI_SUBD_AO,    /* analog output */
    COMEDI_SUBD_DI,    /* digital input */
    COMEDI_SUBD_DO,    /* digital output */
    COMEDI_SUBD_DIO,    /* digital input/output */
    COMEDI_SUBD_COUNTER,    /* counter */
    COMEDI_SUBD_TIMER,    /* timer */
    COMEDI_SUBD_MEMORY,    /* memory, EEPROM, DPRAM */
    COMEDI_SUBD_CALIB,    /* calibration DACs and pots*/
    COMEDI_SUBD_PROC,    /* processor, DSP */
    COMEDI_SUBD_SERIAL,    /* serial IO */
    COMEDI_SUBD_PWM    /* pulse width modulation */
};
C:
/*
* Serial TX/RX SPI Q84 transaction, 24-bit data
* This is memory mapped to the memory sub-device
*/
static void serialWriteOPi(struct comedi_device *dev,
    uint32_t chan,
    uint32_t *value)
{
    uint32_t val_value;
    struct bmc_packet_type *packet = kzalloc(SPI_BUFF_SIZE_NOHUNK, GFP_KERNEL | GFP_NOWAIT | GFP_ATOMIC);

    if (!packet) {
        return;
    }

    val_value = value[0];
    if (chan < 2) {
        // send TX packet
        /* use single transfer for all bytes of the complete SPI transaction */
        packet->bmc_byte_t[BMC_CMD] = CMD_CHAR_GO;
        packet->bmc_byte_t[BMC_D0] = (uint8_t) (val_value & 0xff); // serial data
        packet->bmc_byte_t[BMC_D1] = (uint8_t) chan; // serial channel
        packet->bmc_byte_t[BMC_D2] = (uint8_t) ((val_value >> 16)&0xff); // serial options
        packet->bmc_byte_t[BMC_D3] = BMC_D3;
        packet->bmc_byte_t[BMC_D4] = BMC_D4;
        packet->bmc_byte_t[BMC_EXT] = BMC_EXT;
        packet->bmc_byte_t[BMC_CKSUM] = CHECKBYTE;
        packet->bmc_byte_t[BMC_DUMMY] = CHECKBYTE;
        bmc_spi_exchange(dev, packet);
    }

    // always check for received data
    packet->bmc_byte_t[BMC_CMD] = CMD_CHAR_GET;
    packet->bmc_byte_t[BMC_D0] = BMC_D0;
    packet->bmc_byte_t[BMC_D1] = (uint8_t) chan;
    packet->bmc_byte_t[BMC_D2] = BMC_D2;
    packet->bmc_byte_t[BMC_D3] = BMC_D3;
    packet->bmc_byte_t[BMC_D4] = BMC_D4;
    packet->bmc_byte_t[BMC_EXT] = BMC_EXT;
    packet->bmc_byte_t[BMC_CKSUM] = CHECKBYTE;
    packet->bmc_byte_t[BMC_DUMMY] = CHECKBYTE;
    bmc_spi_exchange(dev, packet);

    val_value = packet->bmc_byte_r[BMC_DUMMY]; // serial data
    val_value += (packet->bmc_byte_r[BMC_D2] << 8); // serial channel
    val_value += (packet->bmc_byte_r[BMC_D1] << 16); // serial data is fresh

    value[0] = val_value;

    kfree(packet);
}
These packets talk to the Q84 SPI slave and write/read/query the physical UART on the controller ISR for SPI interrupts
PIC18 firmware
C:
    // CHAR_GO_BYTES
    if (serial_buffer_ss.cmake_value) {
        if (serial_buffer_ss.raw_index == CHAR_GO_BYTES) {
            /*
             * uart1 only
             */
            UART1_Write(serial_buffer_ss.data[BMC_D0]);
            data_in2 = 0;
            serial_buffer_ss.cmake_value = false;
            serial_buffer_ss.raw_index = BMC_CMD;
            spi_stat_ss.txdone_bit++; // number of completed packets
            spi_stat_ss.slave_tx_count++;
            data_in2 = 0;
        } else {
            spi_stat_ss.slave_tx_count++;
            data_in2 = 0;
        }
    }

    // CHAR_GET_BYTES
    if (serial_buffer_ss.cget_value) {
        if (serial_buffer_ss.raw_index == CHAR_GET_BYTES) {
            /*
             * uart1 only
             */
            if (UART1_is_rx_ready()) {
                tmp_buf = UART1_Read();
            } else {
                tmp_buf = 0;
            }
            SPI2TXB = tmp_buf;
            serial_buffer_ss.cget_value = false;
            serial_buffer_ss.raw_index = BMC_CMD;
            spi_stat_ss.txdone_bit++; // number of completed packets
            data_in2 = 0;
        } else {
            spi_stat_ss.slave_tx_count++;
            if (serial_buffer_ss.raw_index == BMC_D0) {
                tmp_buf = 0x00; //
            } else {
                tmp_buf = UART1_is_rx_ready(); // new data is ready
            }
            SPI2TXB = tmp_buf;
            data_in2 = 0;
        }
    }

    if (!V.do_fail && command == CMD_CHAR_GET) { // send the serial buffer
        spi_comm_ss.ADC_RUN = false;
        spi_comm_ss.PORT_DATA = false;
        spi_comm_ss.CHAR_DATA = true;
        spi_stat_ss.char_count++;
        serial_buffer_ss.raw_index = BMC_D0;
        serial_buffer_ss.cget_value = true;
        spi_comm_ss.REMOTE_LINK = true;
        TMR0_Reload();
    }

    if (!V.do_fail && command == CMD_CHAR_GO) { // get data for the serial buffer
        spi_comm_ss.ADC_RUN = false;
        spi_comm_ss.PORT_DATA = false;
        spi_comm_ss.CHAR_DATA = true;
        spi_stat_ss.char_count++;
        channel = data_in2;
        serial_buffer_ss.raw_index = BMC_D0;
        serial_buffer_ss.cmake_value = true;
        spi_stat_ss.slave_tx_count++;
        spi_comm_ss.REMOTE_LINK = true;
        TMR0_Reload();
    }
The DAQ test program "bmc.c" exercises all sub-devices at close to max speed to check for possible hardware and software issues.

Test program fragments for testing the serial interface. It just send 0x57 to the physical port and reads the port for new data.
C:
    subdev_serial0 = comedi_find_subdevice_by_type(it, COMEDI_SUBD_MEMORY, subdev_serial0);
    if (subdev_serial0 < 0) {
        SERIAL_OPEN = false;
    }

    if (SERIAL_OPEN) {
        fprintf(fout, "Subdev SER %i ", subdev_serial0);
        channels_serial0 = comedi_get_n_channels(it, subdev_serial0);
        fprintf(fout, "Digital Channels %i ", channels_serial0);
        maxdata_serial0 = comedi_get_maxdata(it, subdev_serial0, i);
        fprintf(fout, "Maxdata %i ", maxdata_serial0);
        ranges_serial0 = comedi_get_n_ranges(it, subdev_serial0, i);
        fprintf(fout, "Ranges %i \r\n", ranges_serial0);
    }

    if (SERIAL_OPEN) {
        serial_buf = 0x57;

        comedi_data_write(it, subdev_serial0, 0, range_ao, AREF_GROUND, serial_buf);
        comedi_data_read(it, subdev_serial0, 0, range_ao, AREF_GROUND, &serial_buf);
    }

        fprintf(fout, "%s Sending Comedi data to MQTT server, Topic %s DO 0x%.4x DI 0x%.6x\n", log_time(false), topic_p, bmc.dataout.dio_buf, datain);
        if (bmc.BOARD == bmcboard) {
            fprintf(fout, "ANA0 %lfV, ANA1 %fV, ANA2 %f, ANA4 %fV, ANA5 %fV, AND5 %fV : Scaler Index %d, Scaler ANA4 %f, Scaler ANA5 %f, Serial 0X%X\n",
                get_adc_volts(channel_ANA0), get_adc_volts(channel_ANA1), get_adc_volts(channel_ANA2),
                E.adc[channel_ANA4], E.adc[channel_ANA5], E.adc[channel_AND5], ha_daq_host.hindex, ha_daq_host.scaler4[ha_daq_host.hindex], ha_daq_host.scaler5[ha_daq_host.hindex],
                serial_buf);
        } else {
            fprintf(fout, "ANA0 %lfV, ANA1 %fV : Scaler Index %d, Scaler ANA4 %f, Scaler ANA5 %f\n",
                get_adc_volts(channel_ANA0), get_adc_volts(channel_ANA1),
                ha_daq_host.hindex, ha_daq_host.scaler4[ha_daq_host.hindex], ha_daq_host.scaler5[ha_daq_host.hindex]);
        }
1753205033873.png
1753203791942.png
Clip to loop TX to RX connected and disconnected. It returns 16-bit data, so the UART status for fresh data is the high byte, 0x01xx
1753203844509.png
1753203882380.png

Here we have the module load kernel kprint status, the bmc test program logging as to becomes a background daemon process and the daq_bmc /sys/modules/daq_bmc/parameters of the configuration and various sub-device transaction counts.
1753204241860.png
Linux SPI protocol kernel driver
C:
static int32_t daqbmc_conf = picsl12; // value 0
module_param(daqbmc_conf, int, S_IRUGO);
MODULE_PARM_DESC(daqbmc_conf, "hardware configuration: default 0=bmcboard standard, 1=bmcboard without DI or DO");
static int32_t di_conf = 1; // default true
module_param(di_conf, int, S_IRUGO);
MODULE_PARM_DESC(di_conf, "make digital input subdevice");
static int32_t do_conf = 1; // default true
module_param(do_conf, int, S_IRUGO);
MODULE_PARM_DESC(do_conf, "make digital output subdevice");
static uint32_t ai_count = 0;
module_param(ai_count, uint, S_IRUGO);
MODULE_PARM_DESC(ai_count, "total adc samples");
static uint32_t ao_count = 0;
module_param(ao_count, uint, S_IRUGO);
static uint32_t do_count = 0;
module_param(do_count, uint, S_IRUGO);
static uint32_t di_count = 0;
module_param(di_count, uint, S_IRUGO);
static uint32_t serial_count = 0;
module_param(serial_count, uint, S_IRUGO);
MODULE_PARM_DESC(ao_count, "total dac samples");
static uint32_t hunk_count = 0;
module_param(hunk_count, uint, S_IRUGO);
static int32_t hunk_len = HUNK_LEN;
module_param(hunk_len, int, S_IRUGO);
static int32_t bmc_autoload = 1;
module_param(bmc_autoload, int, S_IRUGO);
MODULE_PARM_DESC(bmc_autoload, "boot autoload: default 1=load module");
static int32_t bmc_type = 0;
module_param(bmc_type, int, S_IRUGO);
MODULE_PARM_DESC(bmc_type, "i/o board type: default 0=bmcboard");
static int32_t bmc_rev = 3;
module_param(bmc_rev, int, S_IRUGO);
MODULE_PARM_DESC(bmc_rev, "board revision: default 3=OPI Zero 3 4G");
static int32_t speed_test = 0;
module_param(speed_test, int, S_IRUGO);
MODULE_PARM_DESC(speed_test, "sample timing test: 1=enable");
static int32_t special_test = 0;
module_param(special_test, int, S_IRUGO);
MODULE_PARM_DESC(special_test, "special timing test: 1=enable");
static int32_t lsamp_size = 0;
module_param(lsamp_size, int, S_IRUGO);
MODULE_PARM_DESC(lsamp_size, "16 or 32 bit lsampl size: 0=16 bit");
static int32_t use_hunking = 0;
module_param(use_hunking, int, S_IRUGO);
https://github.com/nsaspook/mqtt_comedi/tree/cleanup
1753206661119.png
1753206541775.png
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
One of the cool things about these types of Linux drivers is the ability to run DAQ tasks on isolated and dedicated cores.
A C dds program has programmed the DAC channel for asynchronous operation. https://www.ni.com/en/shop/electron...rstanding-direct-digital-synthesis--dds-.html

1753319386877.png
This starts a kernel thread per the driver to handle the I/O from the program, to the Q84 internal DAC module
1753319505898.png
1753319482674.png
Here we see that core 2 process running: daqbmc_d/2
1753319561577.png

Various dds program output signals from the 8-bit DAC.
1753319722577.png
1753319787330.png
Not very fast but more than adequate for simple analog control signals.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333

TIC12400 (24-bit DI) and MC33996 (16-bit DO) full system loop (Q84 SPI2 I/O to the OPi routed via Q84 SPI1 to the DIO chips) driver testing.

I had to use something as the I/O standard. The ETT 10 pin boards are pretty cheap and have lasted many years testing various designs.
https://www.ett.co.th/product/13A02.html
https://www.ett.co.th/product/13A01.html

Waiting for the, hopefully, final JLC board spin with MODBUS and the FM80 charge controller on the Q84 DAQ PCB with the complete back-end server services and front-end Home Assistant HID interface on the Orange Pi Zero 3. This board and the Pi host will have all of the hardware I/O and software needed to control and monitor time-shifting hybrid solar energy systems with up to 48VDC battery banks.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Full system test-bed using the new DAQ board
1756214049061.png
1756214009642.png
1756213332076.png
EM540 energy monitor
1756213405723.png
FM80/60 charge controller
1756213738485.png
1756213756495.png
1756213772481.png

The needed sequence of commands and responses is handled on the 8-bit controller by a FSM, a set of callbacks and interrupt routines to TX and RX to format the returned data to the proper CSV format to send
to the Orange PI via the SPI link.

C:
    const uint16_t cmd_id[] = {0x100, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02};
    const uint16_t cmd_status[] = {0x100, 0x02, 0x01, 0xc8, 0x00, 0x00, 0x00, 0xcb};
    const uint16_t cmd_mx_status[] = {0x100, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05};
    uint16_t cmd_mx_log[] = {0x100, 0x16, 0x00, 0x00, 0x00, 0x01, 0x00, 0x17}; // get logs, start from day 1
    const uint16_t cmd_panelv[] = {0x100, 0x02, 0x01, 0xc6, 0x00, 0x00, 0x00, 0xc9};
    const uint16_t cmd_batteryv[] = {0x100, 0x02, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0a};
    const uint16_t cmd_batterya[] = {0x100, 0x02, 0x01, 0xc7, 0x00, 0x00, 0x00, 0xca};
    const uint16_t cmd_watts[] = {0x100, 0x02, 0x01, 0x6a, 0x00, 0x00, 0x00, 0x6d};
    const uint16_t cmd_misc[] = {0x100, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}; // example FM80 command ID request
    const uint16_t cmd_fwreva[] = {0x100, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04};
    const uint16_t cmd_fwrevb[] = {0x100, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x05};
    const uint16_t cmd_fwrevc[] = {0x100, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06};
    uint16_t cmd_time[] = {0x100, 0x03, 0x40, 0x04, 0x00, 0x00, 0x00, 0x47};
    uint16_t cmd_date[] = {0x100, 0x03, 0x40, 0x05, 0x00, 0x00, 0x00, 0x48}; // set to 5
    const uint16_t cmd_restart_ngit[] = {0x100, 0x03, 0x00, 0xd6, 0x00, 0x00, 0x00, 0xd9}; // using non-GTI mode
    const uint16_t cmd_restart_gti[] = {0x100, 0x03, 0x00, 0xd6, 0x00, 0x01, 0x00, 0xda}; // using GTI mode
    const uint16_t cmd_restart[] = {0x100, 0x03, 0x40, 0x02, 0x00, 0x01, 0x00, 0x46}; // restart command

static void send_mx_cmd(const uint16_t *);
static void rec_mx_cmd(void (* DataHandler)(void), const uint8_t);
/*
 * callbacks to handle FM80 register data
 */
void state_init_cb(void);
void state_status_cb(void);
void state_panelv_cb(void);
void state_batteryv_cb(void);
void state_batterya_cb(void);
void state_watts_cb(void);
void state_misc_cb(void);
void state_mx_status_cb(void);
void state_mx_log_cb(void);
static void state_fwrev_cb(void);
static void state_time_cb(void);
static void state_date_cb(void);
static void state_restart_cb(void);


/*
 * serial I/O ISR, TMR6 500us I/O sample rate
 * polls the required UART registers for 9-bit send and receive into 16-bit arrays
 */
void FM_io(void)
{
    IO_RF4_SetHigh();
    if (pace++ > BUFFER_SPACING) {
        if (dcount-- > 0) {
            if (tbuf[dstart] > 0xff) { // Check for bit-9
                U2P1L = (uint8_t) tbuf[dstart]; // send with bit-9 high, start of packet
            } else {
                UART2_Write((uint8_t) tbuf[dstart]); // send with bit-9 low
            }
            dstart++;
        } else {
            dstart = 0;
            dcount = 0;
        }
        pace = 0;
    }

    /*
     * handle framing errors
     */
    if (U2ERRIRbits.RXFOIF) {
        rbuf[0] = U2RXB; // read bad data to clear error
        U2ERRIRbits.RXFOIF = 0;
        rdstart = 0; // reset buffer to start
    }

    /*
     * read serial data if polled interrupt flag is set
     */
    if (PIR8bits.U2RXIF) {
        if (U2ERRIRbits.FERIF) {
            // do nothing, will clear auto
        }

        if (rdstart > FM_BUFFER - 1) { // overload buffer index
            rdstart = 0; // reset buffer to start
        }
        if (U2ERRIRbits.PERIF) {
            rdstart = 0; // restart receive buffer when we see a 9-th bit high
            rbuf[rdstart] = 0x0100; // start of packet, bit 9 set
        } else {
            rbuf[rdstart] = 0x00;
        }
        rbuf[rdstart] += U2RXB;
        rdstart++;
    }

    timer_ms_tick(0, 0); // software timers update
    IO_RF4_SetLow();
}

/*
 * buffer received data
 * disabled using critical section interrupts here and it was too long 500us
 * and causing data errors
 */
uint8_t FM_rx(uint16_t * data)
{
    uint8_t count;

    count = rdstart;
    if (count > 0) {
        memcpy(data, (const void *) rbuf, (size_t) (count << 1)); // copy 16-bit values
    }
    rdstart = 0;
    return count;
}

/*
 * transmit the cmd data
 */
static void send_mx_cmd(const uint16_t * cmd)
{
    if (FM_tx_empty()) {
        if (BM.pacing++ > PACE) {
            FM_tx(cmd, CMD_LEN); // send 9-bit command data stream
            BM.pacing = 0;
        }
    }
}

/*
 * process received data from the FM80 9n1 serial in abuf 16-bit buffer array with callbacks
 */
static void rec_mx_cmd(void (* DataHandler)(void), const uint8_t rec_len)
{
    static uint16_t online_count = 0;

    if (FM_rx_ready()) {
        if (FM_rx_count() >= rec_len) {
            online_count = 0;
            if (rec_len == REC_LOG_LEN) {
                FM_rx(cbuf);
            } else {
                FM_rx(abuf);
            }
            BM.FM80_io = false;
            DataHandler(); // execute callback to process data in abuf
        } else {
            if (online_count++ > ONLINE_TIMEOUT) {
                online_count = 0;
                BM.FM80_online = false;
                BM.FM80_io = false;
                cc_mode = STATUS_LAST;
                state = state_init;
            }
        }
    }
    if ((BM.FM80_online == false) && online_count++ > ONLINE_TIMEOUT) {
        online_count = 0;
        BM.FM80_online = false;
        BM.FM80_io = false;
        cc_mode = STATUS_LAST;
        state = state_watts;
        mx_code = 0x0;
        DataHandler();
    }
}

#ifdef MX_MATE
        /*
         * FM80 processing state machine
         */
        switch (state) {
        case state_init:
            send_mx_cmd(cmd_id);
            rec_mx_cmd(state_init_cb, REC_LEN);
            break;
        case state_status:
            if (!BM.fm80_restart) {
                send_mx_cmd(cmd_status);
                rec_mx_cmd(state_status_cb, REC_LEN);
            } else {
                send_mx_cmd(cmd_restart);
                rec_mx_cmd(state_restart_cb, REC_LEN);
            }
            break;
        case state_panel:
            send_mx_cmd(cmd_panelv);
            rec_mx_cmd(state_panelv_cb, REC_LEN);
            break;
        case state_batteryv:
            send_mx_cmd(cmd_batteryv);
            rec_mx_cmd(state_batteryv_cb, REC_LEN);
            break;
        case state_batterya:
            send_mx_cmd(cmd_batterya);
            rec_mx_cmd(state_batterya_cb, REC_LEN);
            break;
        case state_watts:
            send_mx_cmd(cmd_watts);
            rec_mx_cmd(state_watts_cb, REC_LEN);
            break;
        case state_mx_status: // wait for ten second flag in this state for logging
            send_mx_cmd(cmd_mx_status);
            rec_mx_cmd(state_mx_status_cb, REC_STATUS_LEN);
            break;
        case state_fwrev:
            switch (fw_state) {
            case 0:
                send_mx_cmd(cmd_fwreva);
                rec_mx_cmd(state_fwrev_cb, REC_LEN);
                break;
            case 1:
                send_mx_cmd(cmd_fwrevb);
                rec_mx_cmd(state_fwrev_cb, REC_LEN);
                break;
            case 2:
                send_mx_cmd(cmd_fwrevc);
                rec_mx_cmd(state_fwrev_cb, REC_LEN);
            default:
                fw_state = 0;
                break;
            }
            break;
        case state_mx_log: // FM80 log data
            send_mx_cmd(cmd_mx_log);
            rec_mx_cmd(state_mx_log_cb, REC_LOG_LEN);
            break;
        case state_time: // FM80 send time data
            send_mx_cmd(cmd_time);
            rec_mx_cmd(state_time_cb, REC_LEN);
            break;
        case state_date: // FM80 send date data
            send_mx_cmd(cmd_date);
            rec_mx_cmd(state_date_cb, REC_LEN);
            break;
        case state_misc:
            send_mx_cmd(cmd_misc);
            rec_mx_cmd(state_misc_cb, REC_LEN);
            break;
        default:
            send_mx_cmd(cmd_id);
            rec_mx_cmd(state_init_cb, REC_LEN);
            break;
        }

        if (TimerDone(TMR_RESTART)) {
            StartTimer(TMR_RESTART, 30000);
            BM.fm80_restart = false;
        }
#endif

/*
 * testing online status while waiting for 10 second flag callback
 */
void state_misc_cb(void)
{
    if (mx_code == FM80_ID) { // only set FM80 offline here
    } else {
        BM.FM80_online = false;
        cc_mode = STATUS_LAST;
        state = state_init;
        return;
    }
    if (!BM.ten_sec_flag) {
        state = state_misc;
    } else {
        state = state_status;
    }
}

void state_mx_log_cb(void)
{
    BM.log.volts_peak = (int16_t) cbuf[5];
    BM.log.day = (int16_t) cbuf[14];
    BM.log.kilowatt_hours = (int16_t) (((uint16_t) (cbuf[3] & 0xF0) >> 4) | (uint16_t) (cbuf[4] << 4));
    BM.log.kilowatts_peak = (int16_t) (((uint16_t) (cbuf[13] & 0xFC) >> 2) | (uint16_t) (cbuf[12] << 6));
    BM.log.bat_max = (int16_t) (((uint16_t) (cbuf[2] & 0xFC) >> 2) | (uint16_t) ((cbuf[3] & 0x0F) << 6));
    BM.log.bat_min = (int16_t) (((uint16_t) (cbuf[10] & 0xC0) >> 6) | (uint16_t) ((cbuf[11] << 2) | ((cbuf[12] & 0x03) << 10)));
    BM.log.amps_peak = (int16_t) (cbuf[1] | ((cbuf[2] & 0x03) << 8));
    BM.log.amp_hours = (int16_t) (cbuf[9] | ((cbuf[10] & 0x3F) << 8));
    BM.log.absorb_time = (int16_t) (cbuf[6] | ((cbuf[7] & 0x0F) << 8));
    BM.log.float_time = (int16_t) (((cbuf[7] & 0xF0) >> 4) | (cbuf[8] << 4));

    cmd_mx_log[5] = BM.log.select;
    cmd_mx_log[7] = 0x16 + BM.log.select; // update the checksum

    state = state_mx_status;
}

void state_mx_status_cb(void)
{
    volt_f((abuf[11] + (abuf[10] << 8))); // set battery voltage here in volt_whole and volt_frac
    vw = volt_whole;
    vf = volt_fract;
    volt_f((abuf[13] + (abuf[12] << 8))); // set panel voltage here in volt_whole and volt_frac
    pvw = volt_whole;
    pvf = volt_fract;
    if ((abuf[1] &0x0f) > 9) { // check for whole Amp
        abuf[2]++; // add extra Amp for fractional overflow.
        abuf[1] = (abuf[1]&0x0f) - 10;
    }
    if (BM.FM80_online) { // don't update when offline
        bat_amp_whole = abuf[3];
        bat_amp_panel = abuf[2];
        bat_amp_frac = abuf[1];
    }
#ifdef debug_data
    printf("%5d: %3x %3x %3x %3x %3x  SDATA: FM80 Data mode %3x %3x %3x %3x %3x %3x %3x %3x %3x\r\n",
        rx_count++, abuf[0], abuf[1], abuf[2], abuf[3], abuf[4], abuf[5], abuf[6], abuf[7], abuf[8], abuf[9], abuf[10], abuf[11], abuf[12], abuf[13]);
#endif

    if (BM.ten_sec_flag) {
        BM.ten_sec_flag = false;
        if (BM.FM80_online || BM.modbus_online) { // log for MX80 and EM540
            MM_ERROR_C;
            /*
             * log CSV values to the comm ports for data storage and processing
             */
            BM.run_time = lp_filter(BM.run_time, F_run, false); // smooth run-time
            //            snprintf(buffer, 25, "%s", asctime(can_newtime)); // the log_buffer uses this string in LOG_VARS
            //            buffer[DTG_LEN] = 0; // remove newline
            //            snprintf(log_buffer, MAX_B_BUF, log_format, LOG_VARS);
            //            printf("%s", log_buffer); // log to USART

            switch (BM.alt_display) {
            case 3:

                break;
            case 2:

                break;
            case 1:

                break;
            case 0:
            default:

                break;
            }

            //            snprintf(info_buffer, MAX_B_BUF, " Data OK\r\n");
        }
    }
    state = state_fwrev;
}
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Integrated the daq_bmc data from the Q84 'master' interface card into the Home Assistant software running on the Orange Pi zero 3.
1756340346169.png
After the transfer via SPI to the OPi the input is validated to be good before posted the MQTT server as JSON formatted data.

1756339982659.png1756340065257.png
1756340246483.png
1756340282333.png
I'm using a non-synchronized stop/start (only for testing) data stream to test error detection.
C:
        if (++slow_data > SLOW_DATA) {
            slow_data = 0;

            if ((daq_data_index > MAX_STRLEN) || (daq_bmc_data_text[BMC4.pos] == '^')) {
                comedi_data_write(it, subdev_serial0, 4, range_ao, AREF_GROUND, STX); // update daq_bmc data buffer
                daq_data_index = 0;
                strncpy(daq_bmc_data_buf, daq_bmc_data_text, SYSLOG_SIZ);
                BMC4.pos = 0;
            } else {
                comedi_data_read(it, subdev_serial0, 4, range_ao, AREF_GROUND, &daq_bmc_data[BMC4.pos]);
                daq_bmc_data_text[BMC4.pos] = (char) (daq_bmc_data[BMC4.pos] >> 8);
                serial_buf = daq_bmc_data_text[BMC4.pos];
                BMC4.pos++;
                daq_data_index++;
            }
        }
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Data stream with proper sync code enabled and with the proper SPI spacing in the Linux driver between MASTER bytes sent to the Q84 to allow at least 50us between bytes, as to not overrun the 2 byte FIFO in the Q84.
https://manpages.debian.org/testing/linux-manual-4.8/schedule_hrtimeout_range.9.en.html
1756522627208.png
C:
static const uint32_t SPI_GAP = 20000; // time for the Q84 to process each received SPI byte

static int32_t bmc_spi_exchange(struct comedi_device *dev, struct bmc_packet_type * packet)
{
    struct comedi_subdevice *s = dev->read_subdev;
    struct spi_param_type *spi_data = s->private;
    struct spi_device *spi = spi_data->spi;
    int32_t ret = 0;
    ktime_t slower = SPI_GAP;

    if (spi == NULL) {
        ret = -ESHUTDOWN;
        return ret;
    }
    /*
     * use nine spi transfers for the complete SPI transaction
     * we need the inter-byte processing time on the slave side
     * with only a two byte FIFO
     */
    packet->one_t.speed_hz = spi->max_speed_hz;

    /*
     * send the cmd and 4-bit channel data
     */
    packet->one_t.tx_buf = &packet->bmc_byte_t[BMC_CMD];
    packet->one_t.rx_buf = &packet->bmc_byte_r[BMC_CMD];
    packet->one_t.cs_change = false;
    packet->one_t.len = 1;
    spi_message_init_with_transfers(packet->m, &packet->one_t, 1); //one transfer per message
    spi_bus_lock(spi->master);
    ret = spi_sync_locked(spi, packet->m);
    spi_bus_unlock(spi->master);
    if (ret == 0) {
        ret = packet->m->actual_length;
    }
//...
}
1756522674183.png
1756522745749.png

Built another board to parallel the operation system for data and control comparisons on the Orange Pi.
1756523472020.png
1756523580679.png
Idle
1756523719265.png
1756523893548.png
Charging battery
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Time to add some external current sensors to track in/out of the battery bank
200A Hall sensor PCB (200A or 100A Hall current sensors) to ADC input 0. Zero and scale at half of current sensor regulated 5vdc reference voltage to the adc0 channel with a 4.096vdc reference.
1756739393262.png
1756739415633.png
1756739433670.png
1756739611535.png
1756739628830.png


Test setup basic diagram
Solar_all_diagram.jpg
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Enclosure for everything.
1756933758079.png
Basic power and reset wiring. Need to make a few cables for the I/O connector that will be on the back.
1756933787983.png
Isolated power for relay switching, reset button.

1756933858600.png
White or Black bezel?
1756933976363.png

Need to add a few controls on the top.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Now, the fun part, documentation of the interior and harness wiring, so I can remember what I did an hour from now.
1757034543910.png
1757034644301.png
1757034671788.png1757034700670.png
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Calibration of the two 65VDC ADC channels for the Solar Panel input and battery bank voltages on ANA5 and ANA5.

Calibration space (EEPROM and defaults in FLASH) for 4 Orange Pi host controllers and daq_bmc boards.
C:
    /*
     * scale adc result into calibrated units
     * for USB boards and BMC boards use MUI_id scales and offsets
     */
#define HV_SCALE0               83.6f
#define HV_SCALE1               74.4f
#define HV_SCALE2               74.4f
#define HV_SCALE3               83.6f
#define HV_SCALE4               64.1890f
#define HV_SCALE5               64.1415f
#define HV_SCALE4_0             64.2600f
#define HV_SCALE5_0             64.2600f
#define HV_SCALE4_1             64.1890f
#define HV_SCALE5_1             64.1415f
#define HV_SCALE4_2             54.1890f
#define HV_SCALE5_2             54.1415f
#define HV_SCALE4_3             64.1890f
#define HV_SCALE5_3             64.1415f
#define HV_SCALE_RAW            4.096f
#define HV_SCALE_OFFSET         0.0f

#define A200_0_ZERO  2.5216f
#define A200_0_SCALAR  133.05f // BATTERY Amp scalar
#define BSENSOR0 0

#define OVER_SAMP       4

    struct ha_daq_calib_type {
        uint64_t bmc_id[4];
        double offset4[4];
        double scaler4[4];
        double offset5[4];
        double scaler5[4];
    };

    struct ha_daq_hosts_type {
        const char hosts[4][NI_MAXHOST];
        const char clients[4][NI_MAXHOST];
        const char topics[4][NI_MAXHOST];
        const char listen[4][NI_MAXHOST];
        char hname[4][NI_MAXHOST];
        double scaler[4], scaler4[4], scaler5[4];
        uint8_t hindex, bindex;
        uint32_t pacer[4];
        struct ha_daq_calib_type calib;
    };

double get_adc_volts(int chan)
{
    lsampl_t data[16];
    int retval;

    retval = comedi_data_read_n(it, subdev_ai, chan, range_ai, aref_ai, &data[0], 1);
    if (retval < 0) {
        comedi_perror("comedi_data_read in get_adc_volts");
        ADC_ERROR = true;
        return 0.0;
    }
    bmc.adc_sample[chan] = data[0];

    ad_range->min = 0.0f;
    if (bmc.BOARD == bmcboard) {
        if (chan == channel_ANA4 || chan == channel_ANA5) {
            if (chan == channel_ANA4) {
                ad_range->max = ha_daq_host.calib.scaler4[ha_daq_host.bindex];
            } else {
                ad_range->max = ha_daq_host.calib.scaler5[ha_daq_host.bindex];
            }
            ad_range->min = 0.0f;
        } else {
            ad_range->max = HV_SCALE_RAW;
            ad_range->min = 0.0f;
        }
    } else {
        ad_range->max = ha_daq_host.scaler[ha_daq_host.hindex];
    }

    return comedi_to_phys(data[0], ad_range, maxdata_ai);
}
1757274598892.png
1757274654319.png

1757274689448.png
1757274733094.png

Not too shabby and much better than the values from the FM80 charge controller panel voltage (the battery voltage can be calibrated on the FM80 but panel voltage read-back is fixed).
1757275954801.png
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Have the 'boxed' version up and running as a power monitor for the PC in the media room. The Orange Pi Zero3 with Display and keyboard/mouse for local control and monitoring.
1758133376222.png
1758133562769.png
1758133462370.png
1758133408376.png1758133629679.png
OPI in clear case with cooling fan.
1758133422538.png

Monitoring the remote and local DEV systems from my workshop.
1758133525298.png
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Setup wireless power monitoring using Matter over thread. First we need a Thread Border Router to interface WiFi IPV6 to the thread radio IPV6 network.
https://openthread.io/codelabs/openthread-border-router#0
1761067416373.png
https://openthread.io/guides/thread-primer/node-roles-and-types
1761068469201.png
1761066676941.png
Two boards in one, connected via uart. Generic firmware build: https://docs.espressif.com/projects/esp-thread-br/en/latest/dev-guide/build_and_run.html

VS Code can be used with the ESP-IDF extension to customize the source.
https://github.com/espressif/vscode-esp-idf-extension/blob/master/README.md
1761066904635.png

Once the firmware(s) has been loaded on the boards, it's time to setup thread and matter on Home Assistant.
1761067006390.png
1761067074137.png
Adding the Eve Energy device via the HA phone app.
1761067127990.png1761067218552.png

1761067330584.png
1761067348774.png
 
Last edited:

Ya’akov

Joined Jan 27, 2019
10,255
Setup wireless power monitoring using Matter over thread. First we need a Thread Border Router to interface WiFi IPV6 to the thread radio IPV6 network.
https://openthread.io/codelabs/openthread-border-router#0
View attachment 357436
https://openthread.io/guides/thread-primer/node-roles-and-types
View attachment 357437
View attachment 357428
Two boards in one, connected via uart. Generic firmware build: https://docs.espressif.com/projects/esp-thread-br/en/latest/dev-guide/build_and_run.html

VS Code can be used with the ESP-IDF extension to customize the source.
https://github.com/espressif/vscode-esp-idf-extension/blob/master/README.md
View attachment 357429

Once the firmware(s) has been loaded on the boards, it's time to setup thread and matter on Home Assistant.
View attachment 357430
View attachment 357431
Adding the Eve Energy device via the HA phone app.
View attachment 357432View attachment 357433

View attachment 357434
View attachment 357435
That is a juicy dev board. I am having a hard time not clicking on "BUY NOW"—but I have too many dev boards in queue and other possibilities for using Thread and Matter so I have duct taped my hands to the desk. (Don't ask how I am typing this, that would be rude.)
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
1763409501629.png
Q84/OPi solar/energy monitor system: Right side, solar monitor sending data to Home Assistant with a full set on test panels and battery. Left side GTI inverter EM540 energy monitor on another set of panels.
1763412689382.png
Production panels vs BMC test panels voltages.

1763409670262.png
The load (off-grid inverter running one of two redundant PC power supplies) is my programming workstation at idle power.
 
Last edited:
Top