import time
import machine
from micropython import const
from machine import Pin

#class OneWire.py
#
class OneWire:
    CMD_SEARCHROM = 0xf0
    CMD_READROM = 0x33
    CMD_MATCHROM = 0x55
    CMD_SKIPROM = 0xcc
    PULLUP_ON = 1

    def __init__(self, pin):
        self.pin = pin
        self.pin.init(pin.OPEN_DRAIN, pin.PULL_UP)
        self.disable_irq = machine.disable_irq
        self.enable_irq = machine.enable_irq
        self.crctab1 = (b"\x00\x5E\xBC\xE2\x61\x3F\xDD\x83"
                        b"\xC2\x9C\x7E\x20\xA3\xFD\x1F\x41")
        self.crctab2 = (b"\x00\x9D\x23\xBE\x46\xDB\x65\xF8"
                        b"\x8C\x11\xAF\x32\xCA\x57\xE9\x74")


    def reset(self, required=False):
        """
        Perform the onewire reset function.
        Returns True if a device asserted a presence pulse, False otherwise.
        """
        sleep_us = time.sleep_us
        pin = self.pin

        pin(0)
        sleep_us(480)
        i = self.disable_irq()
        pin(1)
        sleep_us(60)
        status = not pin()
        self.enable_irq(i)
        sleep_us(420)
        assert status is True or required is False, "Onewire device missing"
        return status

    def readbit(self):
        sleep_us = time.sleep_us
        pin = self.pin

        pin(1) # half of the devices don't match CRC without this line
        i = self.disable_irq()
        pin(0)
        # skip sleep_us(1) here, results in a 2 us pulse.
        pin(1)
        sleep_us(5) # 8 us delay in total
        value = pin()
        self.enable_irq(i)
        sleep_us(40)
        return value

    def readbyte(self):
        value = 0
        for i in range(8):
            value |= self.readbit() << i
        return value

    def readbytes(self, count):
        buf = bytearray(count)
        for i in range(count):
            buf[i] = self.readbyte()
        return buf

    def readinto(self, buf):
        for i in range(len(buf)):
            buf[i] = self.readbyte()

    def writebit(self, value, powerpin=None):
        sleep_us = time.sleep_us
        pin = self.pin

        i = self.disable_irq()
        pin(0)
        # sleep_us(1) # dropped for shorter pulses
        pin(value)
        sleep_us(60)
        if powerpin:
            pin(1)
            powerpin(self.PULLUP_ON)
        else:
            pin(1)
        self.enable_irq(i)

    def writebyte(self, value, powerpin=None):
        for i in range(7):
            self.writebit(value & 1)
            value >>= 1
        self.writebit(value & 1, powerpin)

    def write(self, buf):
        for b in buf:
            self.writebyte(b)

    def select_rom(self, rom):
        """
        Select a specific device to talk to. Pass in rom as a bytearray (8 bytes).
        """
        self.reset()
        self.writebyte(self.CMD_MATCHROM)
        self.write(rom)

    def crc8(self, data):
        """
        Compute CRC, based on tables
        """
        crc = 0
        for i in range(len(data)):
           crc ^= data[i] ## just re-using crc as intermediate
           crc = (self.crctab1[crc & 0x0f] ^
                  self.crctab2[(crc >> 4) & 0x0f])
        return crc

    def scan(self):
        """
        Return a list of ROMs for all attached devices.
        Each ROM is returned as a bytes object of 8 bytes.
        """
        devices = []
        diff = 65
        rom = False
        for i in range(0xff):
            rom, diff = self._search_rom(rom, diff)
            if rom:
                devices += [rom]
            if diff == 0:
                break
        return devices

    def _search_rom(self, l_rom, diff):
        if not self.reset():
            return None, 0
        self.writebyte(self.CMD_SEARCHROM)
        if not l_rom:
            l_rom = bytearray(8)
        rom = bytearray(8)
        next_diff = 0
        i = 64
        for byte in range(8):
            r_b = 0
            for bit in range(8):
                b = self.readbit()
                if self.readbit():
                    if b: # there are no devices or there is an error on the bus
                        return None, 0
                else:
                    if not b: # collision, two devices with different bit meaning
                        if diff > i or ((l_rom[byte] & (1 << bit)) and diff != i):
                            b = 1
                            next_diff = i
                self.writebit(b)
                if b:
                    r_b |= 1 << bit
                i -= 1
            rom[byte] = r_b
        return rom, next_diff

#class DS18X20.py
CMD_CONVERT = const(0x44)
CMD_RDSCRATCH = const(0xbe)
CMD_WRSCRATCH = const(0x4e)
CMD_RDPOWER = const(0xb4)
PULLUP_ON = const(1)
PULLUP_OFF = const(0)

class DS18X20:
    def __init__(self, onewire):
        self.ow = onewire
        self.buf = bytearray(9)
        self.config = bytearray(3)
        self.power = 1 # strong power supply by default
        self.powerpin = None

    def powermode(self, powerpin=None):
        if self.powerpin is not None: # deassert strong pull-up
            self.powerpin(PULLUP_OFF)
        self.ow.writebyte(self.ow.CMD_SKIPROM)
        self.ow.writebyte(CMD_RDPOWER)
        self.power = self.ow.readbit()
        if powerpin is not None:
            assert type(powerpin) is Pin, "Parameter must be a Pin object"
            self.powerpin = powerpin
            self.powerpin.init(mode=Pin.OUT, value=0)
        return self.power

    def scan(self):
        if self.powerpin is not None: # deassert strong pull-up
            self.powerpin(PULLUP_OFF)
        return [rom for rom in self.ow.scan() if rom[0] in (0x10, 0x22, 0x28)]

    def convert_temp(self, rom=None):
        if self.powerpin is not None: # deassert strong pull-up
            self.powerpin(PULLUP_OFF)
        self.ow.reset()
        if rom is None:
            self.ow.writebyte(self.ow.CMD_SKIPROM)
        else:
            self.ow.select_rom(rom)
        self.ow.writebyte(CMD_CONVERT, self.powerpin)

    def read_scratch(self, rom):
        if self.powerpin is not None: # deassert strong pull-up
            self.powerpin(PULLUP_OFF)
        self.ow.reset()
        self.ow.select_rom(rom)
        self.ow.writebyte(CMD_RDSCRATCH)
        self.ow.readinto(self.buf)
        assert self.ow.crc8(self.buf) == 0, 'CRC error'
        return self.buf

    def write_scratch(self, rom, buf):
        if self.powerpin is not None: # deassert strong pull-up
            self.powerpin(PULLUP_OFF)
        self.ow.reset()
        self.ow.select_rom(rom)
        self.ow.writebyte(CMD_WRSCRATCH)
        self.ow.write(buf)

    def read_temp(self, rom):
        try:
            buf = self.read_scratch(rom)
            if rom[0] == 0x10:
                if buf[1]:
                    t = buf[0] >> 1 | 0x80
                    t = -((~t + 1) & 0xff)
                else:
                    t = buf[0] >> 1
                return t - 0.25 + (buf[7] - buf[6]) / buf[7]
            elif rom[0] in (0x22, 0x28):
                t = buf[1] << 8 | buf[0]
                if t & 0x8000: # sign bit set
                    t = -((t ^ 0xffff) + 1)
                return t / 16
            else:
                return None
        except AssertionError:
            return None

    def resolution(self, rom, bits=None):
        if bits is not None and 9 <= bits <= 12:
            self.config[2] = ((bits - 9) << 5) | 0x1f
            self.write_scratch(rom, self.config)
            return bits
        else:
            data = self.read_scratch(rom)
            return ((data[4] >> 5) & 0x03) + 9

    def fahrenheit(self, celsius):
        return celsius * 1.8 + 32 if celsius is not None else None

    def kelvin(self, celsius):
        return celsius + 273.15 if celsius is not None else None

#DS18B20 data line connected to pin P10
ow = OneWire(Pin(2, Pin.OPEN_DRAIN))
temp = DS18X20(ow)
print("Powermode = ", temp.powermode())
roms = temp.scan()
print(roms)
temp.resolution(roms[0], bits=9)
print("Resolution", temp.resolution(roms[0]))

temp.convert_temp()
while True:
    time.sleep(1)
    for rom in roms:
        print(temp.read_temp(rom), end=" ")
    print()
    temp.convert_temp()