#!/usr/bin/env ruby require "serialport" # # Keep an updated buffer of anchor configurations from cloud # Provide a serial API towards ble-gateway to support update requests # Install: # Most recent ruby version should work e.g. 2.6 or later: # ruby --version # Serial port package/gem is required dependency: # sudo gem install serialport # class AnchorConf # Keep static configuration for now # MAC => config hash # name - popular name # utime - last update time # ... # TODO: Replace by cloud connection CFG = { "09:03:2F:2F:9A:37" => { name: "c2m", utime: 0, kind: :master, :id => 5, :slave_cnt => 3 }, "64:FB:DB:06:4A:2C" => { name: "c2s1", utime: 0, kind: :slave , :master_id => 5, :slave_ix => 0, :slave_cnt => 3 }, "78:31:2E:13:A9:02" => { name: "c2s2", utime: 1, kind: :slave , :master_id => 5, :slave_ix => 1, :slave_cnt => 3 }, "9E:B3:AD:ED:B7:68" => { name: "c2s3", utime: 1, kind: :slave , :master_id => 5, :slave_ix => 2, :slave_cnt => 3 }, "95:A2:35:CD:CA:2F" => { name: "t2", utime: 2, kind: :tag }, "92:A0:75:A8:AE:AA" => { name: "m3", utime: 2, kind: :master, :id => 3, :slave_cnt => 4 }, "5A:7E:15:83:EC:E6" => { name: "s31", utime: 3, kind: :slave , :master_id => 3, :slave_ix => 0, :slave_cnt => 4 }, "28:B8:4F:67:5E:6F" => { name: "s32", utime: 3, kind: :slave , :master_id => 3, :slave_ix => 1, :slave_cnt => 4 }, "99:77:A3:79:79:AD" => { name: "s33", utime: 4, kind: :slave , :master_id => 3, :slave_ix => 2, :slave_cnt => 4 }, "E4:D6:08:63:26:5B" => { name: "s34", utime: 4, kind: :slave , :master_id => 3, :slave_ix => 3, :slave_cnt => 4 }, } # Get all config values as configuration string def all_to_str self.class.to_str(all) end # Get config values with :utime grater than gt_utime as configuration string def recent_to_str(gt_utime) self.class.to_str(recent(gt_utime)) end private # only helpers below ... # Get all config items def all CFG end # Get all config items where :utime is grater than gt_utime def recent(gt_utime) CFG.select { |_, x| x[:utime] > gt_utime } end # Convert MAC formatted as "09:03:2F:2F:9A:37" with colons removed "09032F2F9A37" def self.mac_format(s) s.split(':').join end # Convert GFG key value item to config string # Sample format for master: "\x37\x9A\x2F\x2F\x03\x09,M,1,5;" def self.item_to_str(k, v) s = "" mac = mac_format(k) case v[:kind] when :master "#{mac},M,#{v[:id]},#{v[:slave_cnt]}" when :slave "#{mac},S,#{v[:master_id]},#{v[:slave_ix]},#{v[:slave_cnt]}" when :tag "#{mac},T" end end # Get max utime value of items def self.utime_max(items) items.values.max { |a, b| a[:utime] <=> b[:utime] }[:utime] end # Helper: Convert items to string format def self.to_str(items) ans = "" unless items.empty? then # Items using semicolon as separator ans = items.map { |k, v| item_to_str(k, v) }.join(';') + ";" # add max utime for transmission ans << "TIME:" + utime_max(items).to_s + ";" end # add explicit EOT and semicolon as message end ans << "EOT;" end end # Debug output def show(s) puts s end if ARGV[0] == "help" then puts "usage:" puts "1. Start BLE gateway device connected via USB" puts "2. Determine which device is connected" puts " * Look for \"Product: USB CDC For PALOS\" using e.g. dmesg" puts "3. Start this script with corresponding device as first argument e.g." puts " ./cloud-link.rb /dev/ttyACM0" exit end db = AnchorConf.new # Test: print database records # show "get all:" # show db.all_to_str serial_dev="/dev/ttyACM2" # typical Linux device # serial_dev="/dev/cu.usbmodemC9032F2F9A371" # typical macOS device serial_dev=ARGV[0] if ARGV.size == 1 # Test using regular file # Term1: ./cloud-link.rb # Term2: echo "get:0;" >> ./tmp/pipefile # serial_dev="./tmp/pipefile" show "Start protocol on device: " + serial_dev # Basic test of serial port # sp = SerialPort.new(serial_dev, 115200, 8, 1, SerialPort::NONE) # while true do # message = sp.read(3) # if message # # message.chomp! # puts "message:"+message # end # end # Open protocol towards serial client # # For basic testing ... # macOS e.g. # ioreg -p IOUSB # sudo cu -l /dev/cu.usbmodemC9032F2F9A371 # Linux e.g. # cu -l /dev/ttyACM0 -s 115200 # sp = SerialPort.new(serial_dev, 115200, 8, 1, SerialPort::NONE) buffer = "" cmds = [] loop do # add data to buffer # data = sp.read(1) data = sp.getc # show "data: #{data}" if data then if data.size > 0 then buffer << data else sleep 0.1 end else show "serial closed" exit end # extract zero or more commands while (not buffer.empty?) && buffer.include?(';') do cmd, rest = buffer.split(';', 2) cmds << cmd buffer = rest end # print/execute commands cmds.each do |cmd| show "command: #{cmd}" token, n = cmd.split(':', 2) case token when "nop" show "response: N/A" when "get" # print response resp = db.recent_to_str(n.to_i) show "response: #{resp}" # Note: write seems to give problems at reponse end # sp.write(resp) # Note: loop delay also makes it more robust resp.each_char { |c| sp.putc(c) sleep 0.001 } else show "unsupported command: #{token}" end end cmds = [] end