# Copyright 2025 PostalPortal LLC and Netsyms Technologies LLC
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that
# the following conditions are met:
#   1. Redistributions of source code must retain
#      the above copyright notice, this list of conditions
#      and the following disclaimer.
#   2. Redistributions in binary form must reproduce
#      the above copyright notice, this list of conditions
#      and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#   3. Neither the name of the copyright holder
#       nor the names of its contributors may be used
#       to endorse or promote products derived from
#       this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.


#
# This version communicates over serial.
#

from sys import stdin, exit
from _thread import start_new_thread
from utime import sleep
from machine import Pin

sleep(2)
print("PostalPoint(r) Shipping Kiosk Door Controller")
print("Firmware version 1.1.0")
print("Serial build")

led = Pin(25, Pin.OUT) # Onboard LED
relay = Pin(4, Pin.OUT) # Relay control pin
latchStatus = Pin(15, Pin.IN, Pin.PULL_DOWN) # Read status of door latch switch
latchSend = Pin(14, Pin.OUT) # Send signal to latch switch
latchNotifyCounter = 0 # Don't send latch status constantly
latchNotifyInterval = 50 # Number of main loop executions between notifications
uspsLock = Pin(1, Pin.IN, Pin.PULL_DOWN) # Read arrow lock switch
uspsLockSend = Pin(0, Pin.OUT) # Send to arrow lock switch
uspsLockOpen = False
led.value(0)
relay.value(0)
uspsLockSend.value(1)

sleep(0.2)

# Read status of USPS arrow lock to see if it's NC or NO
# The switch will be depressed when the lock is closed
# Assume it's locked when power is turned on
uspsLockValueWhenLocked = uspsLock.value()

# Read if jumper is set to slow mode
fastMode = True
selectSlow = Pin(18, Pin.IN, Pin.PULL_DOWN)
selectEmit = Pin(17, Pin.OUT)
selectEmit.value(1) # Output on middle pin
sleep(0.2)
fastMode = selectSlow.value() != 1
sleep(0.2)
selectEmit.value(0) # Turn off pin, not needed after boot

bufferSize = 128
buffer = [' '] * bufferSize
bufferEcho = False
bufferNextIn, bufferNextOut = 0,0
terminateThread = False

def bufferSTDIN():
    global buffer, bufferSize, bufferEcho, bufferNextIn, terminateThread
    
    while True:                                 # endless loop
        if terminateThread:                     # if requested by main thread ...
            break                               #    ... exit loop
        buffer[bufferNextIn] = stdin.read(1)    # wait for/store next byte from USB serial
        if bufferEcho:                          # if echo is True ...
            print(buffer[bufferNextIn], end='') #    ... output byte to USB serial
        bufferNextIn += 1                       # bump pointer
        if bufferNextIn == bufferSize:          # ... and wrap, if necessary
            bufferNextIn = 0
#
# instantiate second 'background' thread on RD2040 dual processor to monitor and buffer
# incoming data from 'stdin' over USB serial port using 'bufferSTDIN' function (above)
#
bufferSTDINthread = start_new_thread(bufferSTDIN, ())

#
# function to check if a byte is available in the buffer and if so, return it
#
def getByteBuffer():
    global buffer, bufferSize, bufferNextOut, bufferNextIn
    
    if bufferNextOut == bufferNextIn:           # if no unclaimed byte in buffer ...
        return ''                               #    ... return a null string
    n = bufferNextOut                           # save current pointer
    bufferNextOut += 1                          # bump pointer
    if bufferNextOut == bufferSize:             #    ... wrap, if necessary
        bufferNextOut = 0
    return (buffer[n])                          # return byte from buffer


def unlockDoor():
    print("DO", end="")
    led.value(1)
    relay.value(1)
    if fastMode:
        sleep(0.5)
    else:
        sleep(10)
    relay.value(0)
    led.value(0)
    print("OR")

try:
    if fastMode:
        print("Door mode: Fast")
    else:
        print("Door mode: Slow")
    if uspsLockValueWhenLocked == 0:
        print("Arrow lock switch: Normally Closed or Not Present")
    else:
        print("Arrow lock switch: Normally Open")
    print("Boot complete")
    while True:
        if uspsLock.value() != uspsLockValueWhenLocked:
            # Arrow lock override, open door, but then don't again until re-locked
            if uspsLockOpen == False:
                uspsLockOpen = True
                print("USPS")
                unlockDoor()
        if uspsLock.value() == uspsLockValueWhenLocked:
            uspsLockOpen = False
        
        buffCh = getByteBuffer()
        if buffCh and (buffCh == "D" or buffCh == "d"):
            unlockDoor()
        
        latchNotifyCounter = latchNotifyCounter + 1
        if latchNotifyCounter > latchNotifyInterval:
            # Read and transmit latch switch status
            latchNotifyCounter = 0
            latchSend.value(1)
            sleep(0.1)
            if latchStatus.value() == 1:
                print("LATCH:1")
            else:
                print("LATCH:0")
            latchSend.value(0)
            
        sleep(0.1)

except KeyboardInterrupt:                   # trap Ctrl-C input
    terminateThread = True                  # signal second 'background' thread to terminate 
    exit()