And although I haven't moved it from my workbench Pi4 system to the real
mill a few years ago I did create the code to talk to a CANopen module that
has PWM and relay drivers.  USB as Serial port to CANUSB.
https://canusbshop.com/

Written in Python but marked as .txt so it won't scare mail programs.  It
all appeared to work.  Then life got in the way and as yet have not yet
finished off the mist coolant system nor the power draw bar.
John



> -----Original Message-----
> From: marcus.bow...@visible.eclipse.co.uk
> [mailto:marcus.bow...@visible.eclipse.co.uk]
> Sent: December 19, 2024 2:25 PM
> To: Enhanced Machine Controller (EMC)
> Subject: Re: [Emc-users] Biting the bullet
> 
> On 2024-12-19 19:50, John Dammeyer wrote:
> > Ha ha.  I'm still running LinuxCNC 2.8 on my mill.  Kinda afraid to
> > upgrade.  Like if it works why change?
> > John
> 
> Me too....same reasons...
> Marcus
> 
> 
> _______________________________________________
> Emc-users mailing list
> Emc-users@lists.sourceforge.net
> https://lists.sourceforge.net/lists/listinfo/emc-users

Attachment: custom.hal
Description: Binary data

#!/usr/bin/python

# Project LinuxCNC Python CANUSB Serial Port control derived from:
# 
https://forum.linuxcnc.org/10-advanced-configuration/538-controlling-coolant-w-serial-port?start=0
# User: mozmck  (Moses McKnight)

# This application has been modified to use the Lawicel CANUSB treated as a USB 
serial port
# sending out CAN messages in the CANopen format.
#
# References:
# http://linuxcnc.org/docs/html/hal/halmodule.html

# Version 0.2 12SEP21
# Written by 
#   John Dammeyer
#   jo...@autoartisans.com
#
# Version 0.2 11SEP21
#   -- Moved while ser.inWaiting(): into main try loop because CANUSB faults 
with buffer overflow
#      if the on change event PDO is not sent while the bus is active with 
other messages.
#   -- Next step is to create a seperate receive function which parses received 
messages for input
#      and another one that puts out a periodic NMT heartbeat message.


import hal
import time, serial

# *** Issue this command from the terminal
#sudo apt-get install python-serial

# The serial-relays.py needs to be in the config directory for your mill.
# For example:  '/home/jerrybaca/emc2/configs/harborfrieghtmill' 
# Don't forget to sudo chmod +x serial-relays.py

# *** Next Place the following lines into one of the Post HAL files.
#     custom.hal is probably a good enough place.
#loadusr -Wn serial-relays ./serial-relays.py 

#net coolant-mist <= iocontrol.0.coolant-mist
#net coolant-flood <= iocontrol.0.coolant-flood

#net coolant-mist => serial-relays.relay1C
#net coolant-flood => serial-relays.relay1D


# *** End of HAL file instructions.

# --- Global Variables ---
h = hal.component("serial-relays")
h.newpin("relay1A", hal.HAL_BIT, hal.HAL_IN)
h.newpin("relay1B", hal.HAL_BIT, hal.HAL_IN)
h.newpin("relay1C", hal.HAL_BIT, hal.HAL_IN)
h.newpin("relay1D", hal.HAL_BIT, hal.HAL_IN)
h.newpin("relay1E", hal.HAL_BIT, hal.HAL_IN)
h.newpin("relay1F", hal.HAL_BIT, hal.HAL_IN)
h.newpin("relay1G", hal.HAL_BIT, hal.HAL_IN)
h.newpin("relay1H", hal.HAL_BIT, hal.HAL_IN)
h.newpin("mister_pwm", hal.HAL_U32, hal.HAL_IN)
h.newpin("fanspd_pwm", hal.HAL_U32, hal.HAL_IN)
h.hewpin("switch1A", hal.HAL_BIT, hal.HAL_OUT)
h.hewpin("switch1B", hal.HAL_BIT, hal.HAL_OUT)
h.ready()


# Serial stuff, change the serial port address by changing /dev/ttyS0 or COMx 
(for Windows)
# For USB based Serial dongles that use FTDI hardware /dev/ttyUSB0 is often the 
default.
# USB can be set for 115200 baud as long as the target hardware can support 
that since
# it doesn't really delay the LinuxCNC side.
ser = serial.Serial('/dev/ttyUSB0', 115200, bytesize=8, parity='N', stopbits=1, 
timeout=0, xonxoff=0, rtscts=0)

# Create some Global variables for readability and education
RPDO1 = 0x200   # Receive Process Data Object #1
RPDO2 = 0x300   # Receive Process Data Object #2
TPDO2 = 0x280   # Transmit Process Data Object #2
NodeID = 0x18   # xIM series Device Node ID (0x01..0x7F)
PDO_ID = ''     # The character string that holds the OR operation of the PDO+ID

CANopenPDO_1_Msg = ''  # For reporting what we send and so we don't have to 
recreate it each time.
CANopenPDO_2_Msg = ''  # For reporting what we send and so we don't have to 
recreate it each time.
TempStr = ""    # global so we can print what was sent.

# Message Counter used to generate an 'F' command to read flags in case the 
0x07 is missed.
MsgCount = 0
TimeTick = 0

# Copies of CAN dataRelayImage = 0  # No relays set yet.
Mist_PWM = 0    # Mister pump motor speed 0
Fan_PWM = 0     # Cooling fan off.

# Individual Relay State values
rA = 0
rB = 0
rC = 0
rD = 0
rE = 0
rF = 0
rG = 0
rH = 0
# Switch input state values.  Set from received PDO CAN message
sA = 0
sB = 0

# --- Methods ---

# Function to send data to CANUSB dongle.  
# Parameters:
#   v1 is 0-100% PWM value placed into first 1 byte of PDO message
#   v2 is 0-100% PWM value placed into next byte of PDO message
# Outputs:
#   Sends CANopen RPDO_1 message to CANUSB.
def Send_PDO_1(v1, v2):
    global CANopenPDO_1_Msg      # the message without the data.
    global TempStr
     
    ser.write(CANopenPDO_1_Msg.encode('utf-8'))  # send PDO 3xx to node 0x18 
with 1 byte
    TempStr = "{0:0{1}X}".format(v1 & 0xFF,2)
    ser.write(TempStr.encode('utf-8'))  # Add PWM to message
    TempStr = "{0:0{1}X}".format((v1 >> 8) & 0xFF,2)
    ser.write(TempStr.encode('utf-8'))  # Add PWM to message
    ser.write(CANopenPDO_1_Msg.encode('utf-8'))  # send PDO 3xx to node 0x18 
with 1 byte
    TempStr = "{0:0{1}X}".format(v2 & 0xFF,2)
    ser.write(TempStr.encode('utf-8'))  # Add PWM to message
    TempStr = "{0:0{1}X}".format((v2 >> 8) & 0xFF,2)
    ser.write(TempStr.encode('utf-8'))  # Add PWM to message
    ser.write('\r'.encode('utf-8'))     # Tell CANUSB to send message with <CR> 
character.

# Function to send data to CANUSB dongle.  
# Parameters:
#   r is relayimage placed into first byte of PDO message.
# Outputs:
#   Sends CANopen RPDO_2 message to CANUSB.
# Future Enhancements:
#   Pass PDO #, Node ID #, Array of bytes, Number of valid bytes.
def Send_PDO_2(r):
    global CANopenPDO_2_Msg      # the message without the data.
    global TempStr
     
    ser.write(CANopenPDO_2_Msg.encode('utf-8'))  # send PDO 3xx to node 0x18 
with 1 byte
    # Format bit image to two hex characters
    TempStr = "{0:0{1}X}".format(r,2)
    ser.write(TempStr.encode('utf-8'))  # Add relay bit map to message
    TempStr = "{0:0{1}X}".format(v,4)
    ser.write(TempStr.encode('utf-8'))  # Add fan speed to message
    ser.write('\r'.encode('utf-8'))     # Tell CANUSB to send message with <CR> 
character.

# Function to update local PDO RelayImage
# Parameters:
#   r is which relay to switch (1..8) 
#      does not validate relay # to see if in range.
#   level is TRUE or FALSE for relay ON or OFF.
# Modifies:
#   Global RelayImage bitmap of relay values
#
def Update_Relays(r, level):
    global RelayImage         # if this isn't here then the OR operation fails 
with uninitialized variable.
    # Create the text value of the relay image.
    bitmask = 1 << (r-1)    # Select which relay to switch ON.
    if level == True:
        RelayImage = (RelayImage | bitmask)
    else:
        RelayImage = (RelayImage &  ~bitmask)
# End of Update_Relays

# Function to update local Mister Pump PWM value
# Parameters:
#   v is which new value range checked to keep within 100%     
#   value should be 0..100% 
# Modifies:
#   Global Mist_PWM
#
def Update_Mist_PWM( v ):
    global Mist_PWM
    if (v > 100):
       Mist_PWM = 100
    else:
       Mist_PWM = v
# End of Update_Fan_PWM
    
# Function to update local Cooling Fan Speed PWM value
# Parameters:
#   v is which new value range checked to keep within 100%      
#   value should be 0..100% 
# Modifies:
#   Global Fan_PWM
#
def Update_Fan_PWM( v ):
    global Fan_PWM
    if (v > 100):
       Fan_PWM = 100
    else:
       Fan_PWM = v
# End of Update_Fan_PWM
 
# Function to Parse CANopen messages
# Parameters:
#   s is the CANUSB format message for
#    ID  SIZE DATA
#    iii  l    xx xx xx xx xx xx xx xx
# Outputs:
#   If it receives an NMT message with Node in PRE-OP state it sends a GO 
OPERATIONAL message.
#
def ParseCANopenMsg(s): # pull apart ascii CANopen message
        print('Received CANopen Msg = '+s) # for now just print it.
        CANcmd = int(s[0:3],16) & 0x0780  # Pull out Type of CANopen message
        CANid = int(s[1:3],16)            # Get the Node ID
        CANdlc = int(s[3])                # How many bytes in the message
        CANdata = [0] * 8
#        print("CAN cmd", hex(CANcmd), " CAN ID ", hex(CANid), " CANdlc ", 
hex(CANdlc))
        for x in range(CANdlc):           # # Fill in data bytes. 
            CANdata[x] = int(s[4+x*2:6+x*2], 16)  # Would be more efficient 
with an array.
            # print(s[4+x*2:6+x*2])

        # Handle Device Heartbeat messages.
        if (CANcmd == 0x700):
            CANdev_NMT_State = CANdata[0] # int(s[4:6],16) # Get the first byte
            if (CANdev_NMT_State == 0x7f):
                msg = "t00020100\r"  # NMT Master GO Operational for everyone.
                ser.write(msg.encode('utf-8'))
                print("Sent NMT_GO_OPERATIONAL Message = " + msg)
# End of ParseCANopenMsg


 
# --- Mainline loop ---
try:
    # Create text value of CAN Standard ID => PDO + Node ID and Message Length 
of 1
    PDO_ID = "{0:0{1}X}".format(RPDO1+NodeID,3)
    CANopenPDO_1_Msg = 't' + PDO_ID + '4'      # t2183
    PDO_ID = "{0:0{1}X}".format(RPDO2+NodeID,3)
    CANopenPDO_2_Msg = 't' + PDO_ID + '1'      # t3181

    # Initialize CANUSB dongle
    ser.write('C\r'.encode('utf-8'))    # Close first
    ser.write('F\r'.encode('utf-8'))    # Clear Error Flags by reading them
    ser.write('S5\r'.encode('utf-8'))   # set 250kbps
    ser.write('O\r'.encode('utf-8'))    # Open CANUSB for communication.
    
    Send_PDO_1(Mist_PWM, Fan_PWM)       # Update device PWM with default values.
    Send_PDO_2(RelayImage)              # Update device relays with default 
values.
    
    # Main Loop
    while 1:
    
        # Flush messages to find the first one after a CR character.            
        ser.read_until(expected='\r')
        s = ''
        
        # Check for messages from CANUSB.
        while (ser.in_waiting != 0):
            ch = ser.read(1)
            if (ch == b'\r'):  # End of line message found. 
                if (len(s) >= 1):
                    ch = ord(s[0])  # check the first character in the line.
#                    print('Len is = '+str(len(s))+ '. First char is = 
'+str(ch))                    
                    if (ch == ord('z')): # scrap 'z' response from CANUSB which 
means successful transmit..
                        pass
                    elif (ch == ord('F')): # 'F' response 
                        print('Flags = '+str(s))            # tell us what it 
is.
                    elif (ch == ord('t')): # 't' CAN message
                        ParseCANopenMsg(s[1:]) # Parse message while fault 
active and no transmissions.                            
                    elif (ch == 7): # Error occurred so BELL char received.
                        ser.write('F\r'.encode('utf-8'))    # Clear Error Flags 
by reading them
                        print('Fault detected. Request Flags')
                    s = ''
            else:  # Accumulate CANUSB data for parsing.
                 s = s + ch.decode("ascii")
                            
        relayChanged = False            # If a relay has changed then this 
flags it.
        
        if (rA != h.relay1A):           # Test the HAL pin
            rA = h.relay1A              # if it's changed then update our image.
            Update_Relays(1, rA)        # Update the Byte Image too
            relayChanged = True         # And flag it so a CANopen PDO will be 
sent.
        if (rB != h.relay1B):
            rB = h.relay1B
            Update_Relays(2, rB)
            relayChanged = True
        if (rC != h.relay1C):
            rC = h.relay1C
            Update_Relays(3, rC)
            relayChanged = True
        if (rD != h.relay1D):
            rD = h.relay1D
            Update_Relays(4, rD)
            relayChanged = True
             
        if (relayChanged):              # Did a relay change?
            Send_PDO_2(RelayImage)        # Update PDO.
            
        # Next check if LinuxCNC user has changed mister pump or fan PWM value
        if ((Mist_PWM != h.mister_pwm) or (Fan_PWM != h.fanspd_pwm))
            Update_Mist_PWM(h.mister_pwm)  # update both 
            Update_Fan_PWM(h.fanspd_pwm)
            Send_PDO_1(Mist_PWM, Fan_PWM)  # and then PDO message on change.
            
        # Now sleep before sending the update again.
        # Ideally here's where we'd set up a higher resolution timer to send 
the periodic
        # message but test the input messages more often.
        time.sleep(0.10)
        TimeTick += 1
        if (TimeTick == 10): # 1 second
            TimeTick = 0;  # Start over
            # Every 60 loops (seconds) send out an F to read error status
            MsgCount += 1                   
            if MsgCount >= 60:  
                MsgCount = 0
                ser.write('F\r'.encode('utf-8'))    # Clear Error Flags by 
reading them

        

except KeyboardInterrupt:
    raise SystemExit
_______________________________________________
Emc-users mailing list
Emc-users@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/emc-users

Reply via email to