Tuesday, October 4, 2016

Sending Voltage/Amperage Data to ChiliPeppr via Cayenn Protocol Using INA219 on NodeMCU ESP8266

Ok, this is pretty fancy stuff. We are detecting voltage/amperage data from an INA219 current sensor and then feeding it to Serial Port JSON Server and then on to ChiliPeppr so we can show a realtime graph of current sensor data.

The main entry file is:

-- Main run script for ChiliPeppr's Cayenn protocol
-- This is for the Texas Instrument's INA219 current sensor
-- which reports data to ChiliPeppr via the Cayenn protocol
-- so ChiliPeppr can visually show a current sensor graph

--cayenn.init()
--cayenn.sendBroadcas()

ina219.init()

setreport = '{"sampleEveryms": 100, "reportEverySampleCntOf": 10}'

ctr = 0
function onReport(val)
  print("got report:" .. cjson.encode(val))
  ctr = ctr + 1
  if ctr >= 4 then
    print("Got 4 reports so stopping")
    ina219.onPublishReportOff()
  end
  
end

ina219.onPublishReportSet(setreport, onReport)
--ina219.onPublishReportOn()
--ina219.onPublishReportOff()


Then, the cayenn.lua file.


-- UDP Hello Announce v3
--local M = {}
M = {}

M.port = 8988
M.myip = nil
M.sock = nil

--M.announce = 

M.isInitted = false

function M.init(jsonTagTable)
  
  print("Initting...")

  -- figure out if i have an IP
  M.myip = wifi.sta.getip()
  if M.myip == nil then
    print("You need to connect to wifi. Unable to init.")
  else 
    print("My IP: " .. M.myip)
    M.isInitted = true

    -- create socket for outbound UDP sending
    M.sock = net.createConnection(net.UDP, 0)

    -- create server to listen to incoming udp
    M.initUdpServer()
    
    -- create server to listen to incoming tcp
    M.initTcpServer()
    
    -- send our announce
    M.sendBroadcast(jsonTagTable)
  
  end 
  
end

function M.createAnnounce(jsonTagTable)
  
  -- see if there is a jsontagtable passed in as extra meta
  local jsontag = ""
  if jsonTagTable ~= nil then
    ok, jsontag = pcall(cjson.encode, jsonTagTable)
    if ok then
      --print("Adding jsontagtable" .. jsontag)
    else
      print("failed to encode jsontag!")
    end
  end

  local a = {}
  a.Announce = "i-am-a-client"
  a.Widget = "com-chilipeppr-widget-ina219"
  a.MyDeviceId = "chip:" .. node.chipid() .. "-flash:" .. node.flashid() .. "-mac:" .. wifi.sta.getmac()
  a.JsonTag = jsontag
  
  local ok, json = pcall(cjson.encode, a)
  if ok then
    --print("Encoded json for announce: " .. json)
  else
    print("failed to encode json!")
  end
  return json
end

-- send announce to broadcast addr so spjs 
-- knows of our existence
function M.sendBroadcast(jsonTagTable)
  if M.isInitted == false then
    print("You must init first.")
    return 
  end
  
  local bip = wifi.sta.getbroadcast()
  --print("Broadcast addr:" .. bip)
  
  print("Sending announce to ip: " .. bip)
  M.sock:connect(M.port, bip)
  M.sock:send(M.createAnnounce(jsonTagTable))
  M.sock:close()
  
end

function M.setupWifi()
  -- setwifi
  wifi.setmode(wifi.STATION)
  -- longest range is b
  wifi.setphymode(wifi.PHYMODE_B)
  --Connect to access point automatically when in range
  wifi.sta.config("NETGEAR-main", "blah")
  wifi.sta.connect()
  
  --register callback
  wifi.sta.eventMonReg(wifi.STA_IDLE, function() print("STATION_IDLE") end)
  --wifi.sta.eventMonReg(wifi.STA_CONNECTING, function() print("STATION_CONNECTING") end)
  wifi.sta.eventMonReg(wifi.STA_WRONGPWD, function() print("STATION_WRONG_PASSWORD") end)
  wifi.sta.eventMonReg(wifi.STA_APNOTFOUND, function() print("STATION_NO_AP_FOUND") end)
  wifi.sta.eventMonReg(wifi.STA_FAIL, function() print("STATION_CONNECT_FAIL") end)
  wifi.sta.eventMonReg(wifi.STA_GOTIP, M.gotip)
  
  --register callback: use previous state
  wifi.sta.eventMonReg(wifi.STA_CONNECTING, function(previous_State)
      if(previous_State==wifi.STA_GOTIP) then 
          print("Station lost connection with access point\n\tAttempting to reconnect...")
      else
          print("STATION_CONNECTING")
      end
  end)
  
  --start WiFi event monitor with default interval
  wifi.sta.eventMonStart(1000)
  
end

function M.gotip()
  print("STATION_GOT_IP")
  M.myip = wifi.sta.getip()
  print("My IP: " .. M.myip)
end


function M.sendinit()
  -- we need to send a UDP announce packet to every IP
  -- on the subnet to let any possible SPJS know we are
  -- available

  -- iterate thru entire subnet
  M.ipctr = 1
  tmr.alarm(6, 200, tmr.ALARM_SINGLE, M.sendNextHello)

end

function M.sendNextHello()
  
  local i, j = string.find(M.myip, "%.%d+$")
  local prefix = string.sub(M.myip, 0, i)
  local ip = prefix .. M.ipctr
  print("Sending announce to ip: " .. ip)
  M.sock:connect(M.port, ip)
  M.sock:send(M.announce)
  M.sock:close()
  M.ipctr = M.ipctr + 1

  if M.ipctr < 25 then
    tmr.alarm(6, 50, tmr.ALARM_SINGLE, M.sendNextHello)
  end

end

function M.initUdpServer()
  M.udpServer = net.createServer(net.UDP)
  --M.udpServer:on("connection", M.onUdpConnection)
  M.udpServer:on("receive", M.onUdpRecv) 
  M.udpServer:listen(8988)
  print("UDP Server started on port 8988")
end

function M.onUdpConnection(sck)
  print("UDP connection.")
  --ip, port = sck:getpeer()
  --print("UDP connection. from: " .. ip)
end

function M.onUdpRecv(sck, data)
  print("UDP Recvd. data: " .. data)
end

function M.initTcpServer()
  M.tcpServer = net.createServer(net.TCP)
  M.tcpServer:listen(8988, M.onTcpListen)
  
  print("TCP Server started on port 8988")
end

function M.onTcpListen(conn)
  conn:on("receive", M.onTcpRecv)
end

function M.onTcpConnection(sck)
  print("TCP connection.")
  --ip, port = sck:getpeer()
  --print("UDP connection. from: " .. ip)
end

function M.onTcpRecv(sck, data)
  local peer = sck:getpeer()
  print("TCP Recvd. data: " .. data .. ", Peer:" .. peer)
end

function M.subscribe(cmd, callback)
  -- we need to inject this cmd to a table so
  -- we can see if any incoming data matches this cmd
  -- and if so we call the callback with the payload
  if M.subscriptionCmds == nil then
    M.subscriptionCmds = {}
  end
  
  if M.subscriptionCmds[cmd] ~= nil then
    print("Warning. Subscription already existed. Overwriting. cmd:" .. cmd)
  end
  
  M.subscriptionCmds[cmd] = callback
  
  -- debug. comment out for production
  M.printKeys(M.subscriptionCmds)

end

-- We will be passed something like /getVals {timeStart:0, timeEnd:100}
-- Or something like /getVals
-- Or something like /setReports {"samples":10, "intervalms", 100}
function M.parseAndPublish(cmd)
  -- get the signal name which is the first part with the / (slash)
  local i, j = string.find(cmd, " ")
  local signal = string.sub(cmd, 0, i - 1)
  local payload = string.sub(cmd, j + 1)
  print("signal got:\"" .. signal .. "\"")
  print("payload:\"" .. payload .. "\"")
  
  -- see if this cmd is subscribed to
  if M.subscriptionCmds[signal] ~= nil then
    M.subscriptionCmds[signal](payload)
  else
    print("Nobody is subscribed to signal:" .. signal)
  end
  
end

function M.printKeys(table)
  --local keyset={}
  local n=0
  
  print("Subscriptions:")
  for k,v in pairs(table) do
    n=n+1
    --keyset[n]=k
    if k ~= nil then
      print("  Signal:" .. k .. ", Callback:good")
    else
      print("  Signal:" .. k .. ", Callback:empty")
    end
    
  end
  --print(cjson.encode(keyset))
end

--return M

--M.setupWifi()
--M.init()
--M.initServer()
--M.sendBroadcast()

cayenn = M


And then the ina219.lua file...


-- ChiliPeppr INA219 Module ina219.lua v2
--local ina219 = {}
ina219 = {}
ina219.timerid = 0
ina219.id = 0  -- i2c id
ina219.sda = 1
ina219.scl = 2
ina219.devaddr = 0x40   -- 1000000 (A0+A1=GND)

-- Set multipliers to convert raw current/power values
ina219.maxVoltage = 0 -- configured for max 32volts by default after init
ina219.maxCurrentmA = 0 -- configured for max 2A by default after init
ina219.currentDivider_mA = 0 -- e.g. Current LSB = 50uA per bit (1000/50 = 20)
ina219.powerDivider_mW = 0  -- e.g. Power LSB = 1mW per bit
ina219.currentLsb = 0 -- uA per bit
ina219.powerLsb = 1 -- mW per bit

function ina219.init()
  ina219.begin()
  ina219.setCalibration_32V_2A()
  reg = ina219.read_reg_str(0x00)
  print("Config:" .. ina219.getHex(reg))
end

function ina219.getConfig()
  local c = {}
  c.gpiosda = ina219.sda
  c.gpioscl = ina219.scl
  c.i2cdevaddr = ina219.devaddr
  c.maxVoltage = ina219.maxVoltage
  c.maxCurrentmA = ina219.maxCurrentmA
  c.currentDivider_mA = ina219.currentDivider_mA
  c.powerDivider_mW = ina219.powerDivider_mW
  c.currentLsb = ina219.currentLsb
  c.powerLsb = ina219.powerLsb
  return c
end

-- user defined function: read from reg_addr content of dev_addr
function ina219.read_reg_str(reg_addr)
  i2c.start(ina219.id)
  i2c.address(ina219.id, ina219.devaddr, i2c.TRANSMITTER)
  i2c.write(ina219.id,reg_addr)
  i2c.stop(ina219.id)
  tmr.delay(1)
  i2c.start(ina219.id)
  i2c.address(ina219.id, ina219.devaddr, i2c.RECEIVER)
  c=i2c.read(ina219.id, 16) -- read 16bit val
  i2c.stop(ina219.id)
  return c
end

-- returns 16 bit int
function ina219.read_reg_int(reg_addr)
  i2c.start(ina219.id)
  i2c.address(ina219.id, ina219.devaddr, i2c.TRANSMITTER)
  i2c.write(ina219.id,reg_addr)
  i2c.stop(ina219.id)
  tmr.delay(1)
  i2c.start(ina219.id)
  i2c.address(ina219.id, ina219.devaddr, i2c.RECEIVER)
  local c = i2c.read(ina219.id, 16) -- read 16bit val
  i2c.stop(ina219.id)
  -- convert to 16 bit int
  local val = bit.lshift(string.byte(c, 1), 8)
  local val2 = bit.bor(val, string.byte(c, 2))
  return val2
end

function ina219.write_reg(reg_addr, reg_val)
  print("writing reg:" .. reg_addr .. ", reg_val:" .. reg_val)
  i2c.start(ina219.id)
  i2c.address(ina219.id, ina219.devaddr, i2c.TRANSMITTER)
  local bw = i2c.write(ina219.id, reg_addr)
  --print("Bytes written: " .. bw)
  -- upper 8 bits
  local bw2 = i2c.write(ina219.id, bit.rshift(reg_val, 8))
  --print("Bytes written: " .. bw2)
  -- lower 8 bits
  local bw3 = i2c.write(ina219.id, bit.band(reg_val, 0xFF))
  --print("Bytes written: " .. bw3)
  i2c.stop(ina219.id)
end

function ina219.begin()
  -- initialize i2c, set pin1 as sda, set pin2 as scl
  i2c.setup(ina219.id, ina219.sda, ina219.scl, i2c.SLOW)
end

function ina219.reset()
  ina219.write_reg(0x00, 0xFFFF)
end

function ina219.setCalibration_16V_400mA()
  ina219.maxVoltage = 16 
  ina219.maxCurrentmA = 400 
  ina219.currentDivider_mA = 20 -- Current LSB = 50uA per bit (1000/50 = 20)
  ina219.powerDivider_mW = 1  -- Power LSB = 1mW per bit
  ina219.currentLsb = 50 -- uA per bit
  ina219.powerLsb = 1 -- mW per bit
  ina219.write_reg(0x05, 8192)
  -- INA219_CONFIG_BVOLTAGERANGE_16V |
  --                  INA219_CONFIG_GAIN_1_40MV |
  --                  INA219_CONFIG_BADCRES_12BIT |
  --                  INA219_CONFIG_SADCRES_12BIT_1S_532US |
  --                  INA219_CONFIG_MODE_SANDBVOLT_CONTINUOUS;
  -- write_reg(0x05, 0x0000 | 0x0000 | 0x0400 | 0x0018 | 0x0007)
  ina219.write_reg(0x00, 0x41F)
end

function ina219.setCalibration_32V_1A()
  ina219.maxVoltage = 32
  ina219.maxCurrentmA = 1000
  -- Compute the calibration register
  -- Cal = trunc (0.04096 / (Current_LSB * RSHUNT))
  -- Cal = 10240 (0x2800)
  ina219.write_reg(0x05, 10240)
  -- Set multipliers to convert raw current/power values
  ina219.currentDivider_mA = 25   -- Current LSB = 40uA per bit (1000/40 = 25)
  ina219.powerDivider_mW = 1      -- Power LSB = 800uW per bit
  ina219.currentLsb = 40 -- uA per bit
  ina219.powerLsb = 0.8 -- mW per bit
  -- INA219_CONFIG_BVOLTAGERANGE_32V |
  --                  INA219_CONFIG_GAIN_8_320MV |
  --                  INA219_CONFIG_BADCRES_12BIT |
  --                  INA219_CONFIG_SADCRES_12BIT_1S_532US |
  --                  INA219_CONFIG_MODE_SANDBVOLT_CONTINUOUS;
  local config = bit.bor(0x2000, 0x1800, 0x0400, 0x0018, 0x0007)
  ina219.write_reg(0x00, config)
end

function ina219.setCalibration_32V_2A()
  ina219.maxVoltage = 32
  ina219.maxCurrentmA = 2000
  -- Compute the calibration register
  -- Cal = trunc (0.04096 / (Current_LSB * RSHUNT))
  -- Cal = 4096 (0x1000)
  ina219.write_reg(0x05, 4096)
  -- Set multipliers to convert raw current/power values
  ina219.currentDivider_mA = 10 -- Current LSB = 100uA per bit (1000/100 = 10)
  ina219.powerDivider_mW = 1      --Power LSB = 1mW per bit (2/1)
  ina219.currentLsb = 100 -- uA per bit
  ina219.powerLsb = 1 -- mW per bit
  -- INA219_CONFIG_BVOLTAGERANGE_32V |
  --                  INA219_CONFIG_GAIN_8_320MV |
  --                  INA219_CONFIG_BADCRES_12BIT |
  --                  INA219_CONFIG_SADCRES_12BIT_1S_532US |
  --                  INA219_CONFIG_MODE_SANDBVOLT_CONTINUOUS;
  local config = bit.bor(0x2000, 0x1800, 0x0400, 0x0018, 0x0007)
  ina219.write_reg(0x00, config)
end

function ina219.getCurrent_mA()
  -- Gets the raw current value (16-bit signed integer, so +-32767)
  local valueInt = ina219.read_reg_int(0x04)
  return valueInt / ina219.currentDivider_mA
end

function ina219.getBusVoltage_V()
  -- Gets the raw bus voltage (16-bit signed integer, so +-32767)
  local valueInt = ina219.read_reg_int(0x02)
  -- Shift to the right 3 to drop CNVR and OVF and multiply by LSB
  local val2 = bit.rshift(valueInt, 3) * 4
  return val2 * 0.001
end

function ina219.getShuntVoltage_mV()
  -- Gets the raw shunt voltage (16-bit signed integer, so +-32767)
  local valueInt = ina219.read_reg_int(0x01)
  return valueInt * 0.01
end

-- returns the bus power in watts
function ina219.getBusPowerWatts()
  local valueInt = ina219.read_reg_int(0x03)
  return valueInt * ina219.powerLsb
end

function ina219.checkVals()

  reg = ina219.read_reg_str(0x00)
  print("Config: " .. ina219.getHex(reg))

  -- get Shunt Voltage
  --reg = read_reg_int(0x01)
  --print("Shunt Voltage")
  --print(reg)
  --printHex(reg)
  print("Shunt Voltage mV: " .. ina219.getShuntVoltage_mV())

  -- get Bus Voltage
  --reg = read_reg_int(0x02)
  --print("Bus Voltage")
  --print(reg)
  --printHex(reg)
  print("Bus Voltage V: " .. ina219.getBusVoltage_V())

  -- get Power
  reg = ina219.read_reg_int(0x03)
  print("Power: " .. reg)
  --print(reg)
  --printHex(reg)
  print("Power watts: " .. ina219.getBusPowerWatts())

  -- get Current
  --reg = read_reg_int(0x04)
  --print("Current")
  --print(reg)
  --printHex(reg)

  print("Current mA:" .. ina219.getCurrent_mA())

  print("")
end

-- returns an object of vals
function ina219.getVals()
  local val = {}
  val.voltageV = ina219.getBusVoltage_V()
  val.shuntmV = ina219.getShuntVoltage_mV()
  val.powerW = ina219.getBusPowerWatts()
  -- sometimes the ina219 returns false current data
  -- where the value is pegged at max so toss it if the
  -- powerW is at 0 because that is usually when it happens
  if val.powerW == 0 then
    val.currentmA = 0
  else
    val.currentmA = ina219.getCurrent_mA()
  end
  return val
end

function ina219.getHex(val)
  --print("len of val:" .. string.len(val))
  local s = ""
  for i = 1, string.len(val) do
    if string.byte(val, i - 1) then
      s = s .. string.format("%2X", string.byte(val, i - 1)) .. " "
    end
  end
  --print(s)
  return s
end

-- pass in a payload json string of 
-- {"sampleEveryms": 100, "reportEverySampleCntOf": 10}
function ina219.onPublishReportSet(json, callbackOnReport)
  
  tmr.stop(ina219.timerid)
  
  local t = cjson.decode(json)
  
  -- set the callback that is called when the report is complete
  if callbackOnReport == nil then
    print("You did not provide a callback to call when the report is complete. Returning.")
    return
  end
  
  ina219.callbackOnReport = callbackOnReport
  
  if t["sampleEveryms"] ~= nil then
    ina219.sampleEveryms = t["sampleEveryms"]
    print("Setting sample every ms:" .. ina219.sampleEveryms)
  end
  
  if t["reportEverySampleCntOf"] ~= nil then
    ina219.reportEverySampleCntOf = t["reportEverySampleCntOf"]
    print("Setting reportEverySampleCntOf:" .. ina219.reportEverySampleCntOf)
  end
  
  -- reset the sample data
  ina219.sampleCtr = 0
  ina219.sampleData = {}

  -- set the timer
  if tmr.alarm(ina219.timerid, ina219.sampleEveryms, tmr.ALARM_AUTO, ina219.onSample) then
    print("Started sample reporting")
  else
    print("Error starting sample reporting.")
  end
  
end

ina219.sampleCtr = 0
ina219.sampleData = {}
function ina219.onSample()
  
  --print("Doing onSample for ctr:" .. ina219.sampleCtr)
  
  local val = ina219.getVals()
  --print("Sample:" .. cjson.encode(val))
  
  -- get a sample
  ina219.sampleData[ina219.sampleCtr] = val
  ina219.sampleCtr = ina219.sampleCtr + 1
  
  -- see if we are out of reports to grab
  if ina219.sampleCtr >= ina219.reportEverySampleCntOf then
    --print("At max of samples so sending report")
    -- pause reporting just in case it is so fast it interrupts
    -- us while we send the report off to spjs
    tmr.stop(ina219.timerid)
    ina219.callbackOnReport(ina219.sampleData)
    -- reset sample data
    ina219.sampleCtr = 0
    ina219.sampleData = {}
    -- see if we were asked to turn off
    if ina219.isStopping == false then
      tmr.start(ina219.timerid)
    else
      ina219.isStopping = false
    end
  end 
  
  --print("Done with onSample")
end

function ina219.onPublishReportOn()
  tmr.stop(ina219.timerid)
end

ina219.isStopping = false
function ina219.onPublishReportOff()
  ina219.isStopping = true
  tmr.stop(ina219.timerid)
end


--ina219.init()
--return ina219

No comments:

Post a Comment