require 'libusb' # Abstracts access to uBoot avr tiny85 bootloader - can be used only to upload bytes class MicroBoot Functions = [ :get_info, :write_page, :erase_application, :run_program ] # return all thinklets def self.all usb = LIBUSB::Context.new usb.devices.select { |device| device.idVendor == 0x16d0 && device.idProduct == 0x0753 #&& device.product == "\xB5B" }.map { |device| self.new(device) } end def initialize devref @device = devref end def info unless @info result = control_transfer(function: :get_info, dataIn: 4) flash_length, page_size, write_sleep = result.unpack('S>CC') @info = { flash_length: flash_length, page_size: page_size, pages: (flash_length.to_f / page_size).ceil, write_sleep: write_sleep.to_f / 1000.0, version: "#{@device.bcdDevice >> 8}.#{@device.bcdDevice & 0xFF}", version_numeric: @device.bcdDevice } end @info end def erase! puts "erasing" info = self.info control_transfer(function: :erase_application) info[:pages].times do sleep(info[:write_sleep]) # sleep for as many pages as the chip has to erase end puts "erased chip" end # upload a new program def program= bytestring info = self.info raise "Program too long!" if bytestring.bytesize > info[:flash_length] bytes = bytestring.bytes.to_a erase! address = 0 bytes.each_slice(info[:page_size]) do |slice| puts "uploading @ #{address} of #{bytes.length}" control_transfer(function: :write_page, wIndex: address, wValue: slice.length, dataOut: slice.pack('C*')) sleep(info[:write_sleep]) if address == 0 # additional sleep just for first page, as it includes an erase too sleep(info[:write_sleep]) address += slice.length end end def finished info = self.info puts "asking device to finish writing" control_transfer(function: :run_program) puts "waiting for device to finish" # sleep for as many pages as the chip could potentially need to write - this could be smarter info[:pages].times do sleep(info[:write_sleep]) end @io.close @io = nil end def inspect "" end protected # raw opened device def io unless @io @io = @device.open end @io end def control_transfer(opts = {}) opts[:bRequest] = Functions.index(opts.delete(:function)) if opts[:function] io.control_transfer({ wIndex: 0, wValue: 0, bmRequestType: usb_request_type(opts), timeout: 5000 }.merge opts) end def usb_request_type opts c = LIBUSB::Call value = c::RequestTypes[:REQUEST_TYPE_VENDOR] | c::RequestRecipients[:RECIPIENT_DEVICE] value |= c::EndpointDirections[:ENDPOINT_OUT] if opts.has_key? :dataOut value |= c::EndpointDirections[:ENDPOINT_IN] if opts.has_key? :dataIn return value end end class HexProgram def initialize input @bytes = Hash.new(0xFF) input = input.read if input.is_a? IO parse input end def binary highest_address = @bytes.keys.max bytestring = Array.new(highest_address + 1) { |index| @bytes[index] }.pack('C*') end protected def parse input_text input_text.each_line do |line| next unless line.start_with? ':' line.chomp! length = line[1..2].to_i(16) # usually 16 or 32 address = line[3..6].to_i(16) # 16-bit start address record_type = line[7..8].to_i(16) data = line[9.. 9 + (length * 2)] checksum = line[9 + (length * 2).. 10 + (length * 2)].to_i(16) checksum_section = line[1...9 + (length * 2)] checksum_calculated = checksum_section.chars.to_a.each_slice(2).map { |slice| slice.join('').to_i(16) }.reduce(0, &:+) checksum_calculated = (((checksum_calculated % 256) ^ 0xFF) + 1) % 256 raise "Hex file checksum mismatch @ #{line}" unless checksum == checksum_calculated if record_type == 0 # data record data_bytes = data.chars.each_slice(2).map { |slice| slice.join('').to_i(16) } data_bytes.each_with_index do |byte, index| @bytes[address + index] = byte end end end end end if ARGV[0] if ARGV[0].end_with? '.hex' puts "parsing input file as intel hex" test_data = HexProgram.new(open ARGV[0]).binary else puts "parsing input file as raw binary" test_data = open(ARGV[0]).read end else raise "Pass intel hex or raw binary as argument to script" end puts "Plug in programmable device now: (waiting)" sleep 0.5 while MicroBoot.all.length == 0 thinklet = MicroBoot.all.first puts "Attached to device: #{thinklet.inspect}" #puts "Attempting to write '#{test_data.inspect}' to first thinklet's program memory" #puts "Bytes: #{test_data.bytes.to_a.inspect}" puts "Attempting to write supplied program in to device's memory" thinklet.program = test_data puts "That seems to have gone well! Telling thinklet to run program..." thinklet.finished # let thinklet know it can go do other things now if it likes puts "All done!"